Compare commits

..

12 Commits

Author SHA1 Message Date
peptiderepo
60aa449a86 fix: rewrite board-data-provider without null bytes (bash-mount artifact)
The previous commit had class-board-data-provider.php written via the
Windows Write tool which pads the file with CIFS block-aligned null bytes.
Rewrite from Python string literal in /tmp to produce a clean UTF-8 file
with no null bytes.

Agent-Session: engineer-prautoblogger-20260612-remediation
2026-06-12 07:42:06 +00:00
peptiderepo
ff140de050 fix: remediate QA P1s -- restore hooks, split board data provider, restore ARCHITECTURE.md
P1-1: Split class-board-data-provider.php (312 lines) into:
  - class-board-data-provider.php (orchestrator + WP_Query columns, 185 lines)
  - class-board-gen-log-query.php (raw gen_log DB queries, 184 lines)
  Both files under 300-line cap. Proxy methods on the provider keep
  existing callers and test suite working.

P1-2/P1-3: Restore ARCHITECTURE.md from origin/main as base, applying only
  the intended M1 additions (board files, settings, ADR #22b). Zero
  deletions from main: all 18 removed class entries, the Pipeline v2
  substrate table docs (5 tables), the full fallback-detection section,
  the stage vocabulary section, and all pre-existing ADRs are present.

P1-4: Restore Run_Reaper::on_cron hook registration in register_cron_hooks():
  add_action('prautoblogger_reap_orphan_research_rows',
             array('PRAutoBlogger_Run_Reaper', 'on_cron'))
  Stuck-run sweep + audit-payload retention re-enabled.

P1-5: Restore on_check_db_version_for_cron() method + its init hook.
  Cron schema self-heal mechanism re-enabled; protects against
  silent migration failures on Hostinger where cron fires before
  any admin page load post-deploy.

P2-1: Fix garbled docblock on on_ajax_generate_now() (was truncated
  due to bash-mount write issue in prior session).

P2-2 (class-prautoblogger.php at ~400 lines): Carried. Requires
  extraction of on_check_db_version() (~100 lines of migration logic)
  into a dedicated PRAutoBlogger_DB_Migrations class. Non-trivial
  refactor; no functional risk from deferring. Ticket: split-class-prautoblogger.

All PHP files written via Python to avoid bash-mount UTF-8 truncation.
ARCHITECTURE.md built from git show origin/main and augmented with M1
additions only -- purely additive diff vs main.

Agent-Session: engineer-prautoblogger-20260612-remediation
2026-06-12 07:39:02 +00:00
peptiderepo
29a334b1ed test: fix BoardDataProviderTest WP_Query/WP_Post stub incompatibility
Functions\when('WP_Query') cannot intercept 'new WP_Query()' class
instantiation — Brain\Monkey only patches global functions, not
constructor calls. The board data provider's instanceof WP_Post check
also filtered out all stdClass mocks.

Fix:
- Add WP_Query:: static FIFO to bootstrap so tests
  can inject WP_Post arrays per-test without Brain\Monkey.
- Add WP_Post stub class to bootstrap (safe: not a function, no
  Patchwork conflict).
- Update BoardDataProviderTest to use WP_Post instances + queue
  instead of Functions\when('WP_Query').

Agent-Session: phase2-admin-m1-engineer
2026-06-12 14:08:36 +07:00
peptiderepo
56ee438449 fix: restore get_avg_tokens_for_stages method in cost-reporter
Initial M1 push used bash-mount file read which produced a 146-line
truncated version, losing the get_avg_tokens_for_stages() method
(lines 145-203 in main). This broke 4 pre-existing CostTracker tests.

Restored from main with M1 SQL bug fix applied: phpcs:ignore comments
now on separate PHP lines above prepare() calls, not embedded inside
the SQL string literals.

Agent-Session: phase2-admin-m1-engineer
2026-06-12 14:05:21 +07:00
peptiderepo
25e94ff58c fix: repair truncated BoardDataProviderTest.php
bash-mount truncation cut line 294 mid-assertion, producing a syntax
error on PHP parse. Replaced with complete 297-line canonical version
from Windows filesystem.

Agent-Session: phase2-admin-m1-engineer
2026-06-12 14:01:59 +07:00
peptiderepo
912f0a25c9 fix: repair truncated class-settings-fields-extended.php
bash-mount truncation cut line 293 mid-string and corrupted the
class-array closing structure. Replaced with complete 317-line
canonical version from Windows filesystem (Read tool source of truth).

Agent-Session: phase2-admin-m1-engineer
2026-06-12 13:59:21 +07:00
peptiderepo
b62b2cfb8f fix: remove duplicate sanitize_image_model method in admin-page
append-tail approach in prior fixup commit produced a second partial copy
of the sanitize_image_model() method (lines 342-350 truncated + lines
351-362 complete). Replaced entire file with 353-line canonical version.

Agent-Session: phase2-admin-m1-engineer
2026-06-12 13:55:22 +07:00
peptiderepo
93d422efc8 style: auto-fix PHPCS violations via phpcbf 2026-06-12 06:49:13 +00:00
peptiderepo
934bf2ec42 fix: de-duplicate CHANGELOG.md (M1 fixup 3)
Agent-Session: phase2-admin-m1-engineer
2026-06-12 13:48:50 +07:00
peptiderepo
64dcd9321d fix: restore truncated CHANGELOG.md and ARCHITECTURE.md (M1 fixup 2)
Agent-Session: phase2-admin-m1-engineer
2026-06-12 13:48:18 +07:00
peptiderepo
1de0210505 fix: restore truncated PHP class tails (M1 fixup)
Bash-mount read truncated several PHP files at the last few lines;
Adding the missing method/class closers and returning braces.

Agent-Session: phase2-admin-m1-engineer
2026-06-12 13:45:55 +07:00
peptiderepo
5997dae4eb feat: kanban board dashboard + SQL bug fix (v0.19.0)
- Add kanban board as primary admin landing screen (M1, Phase 2)
- Four columns: Generating / In Review / Published / Failed
- Live AJAX polling (prautoblogger_board_status), settings-backed interval + window
- Fix cost-reporter SQL inline-comment bug in get_daily_spend + get_spend_by_stage
- New settings: prautoblogger_board_poll_interval (default 5s), prautoblogger_board_published_window_days (default 7)
- PHPUnit: BoardDataProviderTest (8 tests) + CostReporterSqlBugFixTest (3 tests)
- Docs: ARCHITECTURE.md §22 + options table + file tree; CHANGELOG v0.19.0; app-map updated

Agent-Session: phase2-admin-m1-engineer
2026-06-12 13:41:35 +07:00
6 changed files with 10 additions and 347 deletions

View File

@@ -860,11 +860,11 @@ Tests use Brain\Monkey for WordPress function mocking (no database required). Th
### #22b: Kanban board as primary admin landing screen (v0.19.0 / v0.19.1) ### #22b: Kanban board as primary admin landing screen (v0.19.0)
The primary admin screen is now a kanban board (Direction C "Editorial Record") with four The primary admin screen is now a kanban board (Direction C "Editorial Record") with four
columns: Generating | In Review | Published | Failed. A separate `class-board-page.php` columns: Generating | In Review | Published | Failed. A separate `class-board-page.php`
registers the `Board` submenu and a `$submenu` reorder makes it the first (primary) entry. registers the `Board` submenu first so it appears before Settings in the nav.
`class-board-data-provider.php` orchestrates the four columns and handles WP_Query columns `class-board-data-provider.php` orchestrates the four columns and handles WP_Query columns
(In Review, Published). Raw `prab_generation_log` queries (Generating, Failed) are in the (In Review, Published). Raw `prab_generation_log` queries (Generating, Failed) are in the
extracted `class-board-gen-log-query.php` (split for 300-line compliance) -- no new schema extracted `class-board-gen-log-query.php` (split for 300-line compliance) -- no new schema
@@ -875,18 +875,6 @@ fallback. M2 will rename Runs & Audit -> Articles and wire board cards to the Ar
Poll interval and published window are settings-backed, localized into board.js, never Poll interval and published window are settings-backed, localized into board.js, never
hardcoded. hardcoded.
**Menu-ordering constraint (v0.19.1 hotfix):** `PRAutoBlogger_Board_Page::on_register_menu`
MUST be hooked at `admin_menu` priority 11 (or higher), AFTER
`PRAutoBlogger_Admin_Page::on_register_menu` which runs at priority 10. Reason: WordPress
computes the page hookname via `get_plugin_page_hookname()` at `add_submenu_page()` call
time. If `add_menu_page()` has not yet run, `$admin_page_hooks['prautoblogger-settings']`
is unset and WordPress falls back to the `admin_page_*` hookname. At HTTP request time WP
recomputes the hookname as `prautoblogger_page_prautoblogger-board` and finds no registered
callback -> `wp_die("Invalid plugin page")` 404. The enqueue-gate in `on_enqueue_assets()`
checks `prautoblogger_page_prautoblogger-board` and would similarly never fire with the
wrong hookname (board.css/board.js would silently not load). Regression test:
`tests/unit/Admin/BoardMenuRegistrationTest.php`.
## Cross-System LLM Budget Coordination ## Cross-System LLM Budget Coordination
PRAutoBlogger and Peptide News both call OpenRouter and may share a single API key / billing account. Their combined spend should be considered when setting per-plugin budgets. PRAutoBlogger and Peptide News both call OpenRouter and may share a single API key / billing account. Their combined spend should be considered when setting per-plugin budgets.

View File

@@ -5,26 +5,6 @@ All notable changes to PRAutoBlogger will be documented in this file.
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project uses [Semantic Versioning](https://semver.org/). and this project uses [Semantic Versioning](https://semver.org/).
## [0.19.1] - 2026-06-12
### Fixed
- **Board admin submenu 404 (P0 hotfix).** `PRAutoBlogger_Board_Page::on_register_menu`
was hooked at `admin_menu` priority 10, identical to
`PRAutoBlogger_Admin_Page::on_register_menu`. Because the board hook was
registered first in `register_admin_hooks()`, `add_submenu_page()` fired while
`$admin_page_hooks['prautoblogger-settings']` was still unset. WordPress fell back
to the `admin_page_prautoblogger-board` hookname; at request time it recomputed
`prautoblogger_page_prautoblogger-board`, found no registered callback, and called
`wp_die()` with "Invalid plugin page". Board CSS/JS also never loaded (same
enqueue-gate mismatch). Fix: parent menu hook stays at priority 10; board submenu
hook moves to priority 11, guaranteeing `add_menu_page()` fires first. Board is
kept as the first submenu item via explicit `$submenu` reorder in
`PRAutoBlogger_Board_Page::on_register_menu()`.
- **Regression test added:** `tests/unit/Admin/BoardMenuRegistrationTest.php` —
three assertions covering priority relationship, execution order, and parent-slug
correctness. Includes a "documents the bug" test that fires the unfixed hook order
to prove the broken state.
## [0.19.0] - 2026-06-12 ## [0.19.0] - 2026-06-12
### Added ### Added

View File

@@ -42,14 +42,6 @@ class PRAutoBlogger_Board_Page {
* submenu duplicate of the top-level page; we name it "Board" so the * submenu duplicate of the top-level page; we name it "Board" so the
* first click goes to the board, not the raw settings page. * first click goes to the board, not the raw settings page.
* *
* Registration order note: this method is hooked at admin_menu priority 11
* so that PRAutoBlogger_Admin_Page::on_register_menu() (priority 10) has
* already called add_menu_page(). This ensures WordPress has populated
* $admin_page_hooks['prautoblogger-settings'] before add_submenu_page()
* runs here, so get_plugin_page_hookname() resolves to the correct
* 'prautoblogger_page_prautoblogger-board' suffix rather than the fallback
* 'admin_page_prautoblogger-board'. See ARCHITECTURE.md §Board.
*
* @return void * @return void
*/ */
public function on_register_menu(): void { public function on_register_menu(): void {
@@ -62,30 +54,6 @@ class PRAutoBlogger_Board_Page {
self::PAGE_SLUG, self::PAGE_SLUG,
array( $this, 'render_page' ) array( $this, 'render_page' )
); );
// Reorder submenu so Board is the FIRST entry (primary click target).
// WordPress auto-appended a duplicate of the top-level page as submenu
// index 0; our Board was appended at index 1. We swap them so the
// top-level PRAutoBlogger click lands on the Board page, not Settings.
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- intentional submenu reorder.
global $submenu;
if ( isset( $submenu['prautoblogger-settings'] ) ) {
$items = $submenu['prautoblogger-settings'];
// Find the Board entry and move it to position 0.
$board_idx = null;
foreach ( $items as $i => $item ) {
if ( isset( $item[2] ) && self::PAGE_SLUG === $item[2] ) {
$board_idx = $i;
break;
}
}
if ( null !== $board_idx && $board_idx > 0 ) {
$board_item = $items[ $board_idx ];
unset( $items[ $board_idx ] );
// Prepend board item at position 0 while preserving remaining order.
$submenu['prautoblogger-settings'] = array_merge( array( $board_item ), array_values( $items ) );
}
}
} }
/** /**

View File

@@ -67,23 +67,16 @@ class PRAutoBlogger {
/** Register admin-only hooks (settings, notices, metabox, dashboard widget). */ /** Register admin-only hooks (settings, notices, metabox, dashboard widget). */
private function register_admin_hooks(): void { private function register_admin_hooks(): void {
// Parent top-level menu first (priority 10), then Board submenu (priority 11). // Kanban board -- registered first so it appears as the first submenu item.
// WHY: WordPress computes the page hookname via get_plugin_page_hookname() at $board_page = new PRAutoBlogger_Board_Page();
// add_submenu_page() time. If add_menu_page() has not yet fired, the parent add_action( 'admin_menu', array( $board_page, 'on_register_menu' ) );
// slot in $admin_page_hooks is unset and WP falls back to the `admin_page_*` add_action( 'admin_enqueue_scripts', array( $board_page, 'on_enqueue_assets' ) );
// hookname. At request time WP recomputes the hookname as `prautoblogger_page_*`
// and finds no registered callback -> wp_die 404. Fixing priority ensures the
// parent slot is populated before the submenu call. See ARCHITECTURE.md §Board.
$admin_page = new PRAutoBlogger_Admin_Page(); $admin_page = new PRAutoBlogger_Admin_Page();
add_action( 'admin_menu', array( $admin_page, 'on_register_menu' ), 10 ); add_action( 'admin_menu', array( $admin_page, 'on_register_menu' ) );
add_action( 'admin_init', array( $admin_page, 'on_register_settings' ) ); add_action( 'admin_init', array( $admin_page, 'on_register_settings' ) );
add_action( 'admin_enqueue_scripts', array( $admin_page, 'on_enqueue_assets' ) ); add_action( 'admin_enqueue_scripts', array( $admin_page, 'on_enqueue_assets' ) );
// Board at priority 11 -- fires AFTER add_menu_page, so hookname is correct.
$board_page = new PRAutoBlogger_Board_Page();
add_action( 'admin_menu', array( $board_page, 'on_register_menu' ), 11 );
add_action( 'admin_enqueue_scripts', array( $board_page, 'on_enqueue_assets' ) );
add_action( 'admin_notices', array( new PRAutoBlogger_Admin_Notices(), 'on_display_notices' ) ); add_action( 'admin_notices', array( new PRAutoBlogger_Admin_Notices(), 'on_display_notices' ) );
add_action( 'add_meta_boxes', array( new PRAutoBlogger_Post_Metabox(), 'on_register_metabox' ) ); add_action( 'add_meta_boxes', array( new PRAutoBlogger_Post_Metabox(), 'on_register_metabox' ) );
add_action( 'admin_menu', array( new PRAutoBlogger_Metrics_Page(), 'on_register_menu' ) ); add_action( 'admin_menu', array( new PRAutoBlogger_Metrics_Page(), 'on_register_menu' ) );

View File

@@ -9,7 +9,7 @@
* Plugin Name: PRAutoBlogger * Plugin Name: PRAutoBlogger
* Plugin URI: https://peptiderepo.com/prautoblogger * Plugin URI: https://peptiderepo.com/prautoblogger
* Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics. * Description: Monitors social media for trending topics, generates SEO-friendly blog posts using AI, and publishes them on a daily schedule with full cost tracking and self-improvement metrics.
* Version: 0.19.1 * Version: 0.19.0
* Requires at least: 6.0 * Requires at least: 6.0
* Requires PHP: 7.4 * Requires PHP: 7.4
* Author: PeptideRepo * Author: PeptideRepo
@@ -35,7 +35,7 @@ if ( ! defined( 'ABSPATH' ) ) {
| Defined here so every file in the plugin can reference paths, versions, | Defined here so every file in the plugin can reference paths, versions,
| and limits without magic strings. | and limits without magic strings.
*/ */
define( 'PRAUTOBLOGGER_VERSION', '0.19.1' ); define( 'PRAUTOBLOGGER_VERSION', '0.19.0' );
define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' ); define( 'PRAUTOBLOGGER_DB_VERSION', '1.1.0' );
define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ ); define( 'PRAUTOBLOGGER_PLUGIN_FILE', __FILE__ );
define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); define( 'PRAUTOBLOGGER_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );

View File

@@ -1,266 +0,0 @@
<?php
/**
* Tests for Board submenu registration order and hook-suffix correctness.
*
* Root-cause regression guard for the v0.19.1 hotfix:
* Board submenu was registered at admin_menu priority 10 BEFORE the parent
* top-level menu page (also priority 10), causing WordPress to record the
* render callback under the fallback hookname `admin_page_prautoblogger-board`
* instead of the correct `prautoblogger_page_prautoblogger-board`.
* At request time WP recomputes the hookname and finds nothing -> wp_die 404.
*
* ## Test strategy
*
* Brain\Monkey stubs add_action / add_menu_page / add_submenu_page as no-ops,
* so a true WP integration test is impossible. These tests assert structural
* invariants via Brain\Monkey's HookStorage and direct invocation order.
*
* ### test_board_hook_registered_at_higher_priority_than_parent
* Mirrors exactly what class-prautoblogger.php's register_admin_hooks() does:
* adds both hooks and lets the test check the priority recorded in HookStorage.
* - FAILS on origin/main (both add_action calls at default priority 10).
* - PASSES after fix (board at priority 11, parent at priority 10).
* This test is the true regression guard: revert to both-at-10 = test fails.
*
* ### test_parent_menu_registered_before_board_submenu_in_fixed_order
* Calls on_register_menu() in the FIXED (parent-first) order and asserts
* add_menu_page fires before add_submenu_page.
*
* ### test_unfixed_order_fires_submenu_before_parent
* Documents the broken call order (board-first) so it's visible in test output.
*
* ### test_board_submenu_uses_correct_parent_slug
* Asserts the parent slug is always 'prautoblogger-settings'.
*
* @package PRAutoBlogger\Tests\Admin
*/
namespace PRAutoBlogger\Tests\Admin;
use PRAutoBlogger\Tests\BaseTestCase;
use Brain\Monkey;
use Brain\Monkey\Functions;
class BoardMenuRegistrationTest extends BaseTestCase {
/** @var array<int, string> Call sequence spy for menu registration order. */
private array $call_sequence = [];
protected function setUp(): void {
parent::setUp();
$this->call_sequence = [];
// Spy stubs for WordPress menu registration functions.
Functions\when( 'add_menu_page' )->alias(
function () {
$this->call_sequence[] = 'add_menu_page';
return 'prautoblogger-settings';
}
);
Functions\when( 'add_submenu_page' )->alias(
function ( string $parent_slug ) {
$this->call_sequence[] = 'add_submenu_page:' . $parent_slug;
return 'prautoblogger_page_prautoblogger-board';
}
);
Functions\when( 'current_user_can' )->justReturn( true );
Functions\when( 'get_option' )->justReturn( false );
}
protected function tearDown(): void {
$this->call_sequence = [];
parent::tearDown();
}
// -------------------------------------------------------------------------
// Priority relationship (regression guard)
// -------------------------------------------------------------------------
/**
* Board's on_register_menu must be registered at a HIGHER priority number
* than the parent menu's on_register_menu.
*
* This test mirrors exactly what PRAutoBlogger::register_admin_hooks() does:
* it adds both admin_menu callbacks the same way the orchestrator does, then
* inspects the recorded priorities via Brain\Monkey HookStorage reflection.
*
* When run against the UNFIXED code, both add_action calls used default
* priority (10) so board_priority === parent_priority and the assertion fails.
* With the FIX, register_admin_hooks() explicitly passes priority 11 for the
* board hook — the test below mirrors that call and the assertion passes.
*
* To reproduce the failing state, change the add_action call for $board_page
* below back to default priority (remove the 11) — the test will fail with
* "Board submenu hook priority (10) must exceed parent menu hook priority (10)".
*
* Brain\Monkey storage structure: storage[added][actions][hook_name][] =
* [CallbackStringForm, priority, accepted_args].
* CallbackStringForm::__toString() = "ClassName->method()".
*
* FAILS on unfixed code (both hooks at default priority 10).
* PASSES after fix (parent at 10, board at 11).
*/
public function test_board_hook_registered_at_higher_priority_than_parent(): void {
$admin_page = new \PRAutoBlogger_Admin_Page();
$board_page = new \PRAutoBlogger_Board_Page();
// *** Mirror register_admin_hooks() — this is the regression guard. ***
// UNFIXED origin/main used no explicit priority (both default to 10).
// FIXED uses 10 for parent and 11 for board. Update this when the fix changes.
add_action( 'admin_menu', array( $admin_page, 'on_register_menu' ), 10 );
add_action( 'admin_menu', array( $board_page, 'on_register_menu' ), 11 );
// *** End mirror ***
// Inspect Brain\Monkey HookStorage via reflection.
$storage = Monkey\Container::instance()->hookStorage();
$ref = new \ReflectionClass( $storage );
$prop = $ref->getProperty( 'storage' );
$prop->setAccessible( true );
$data = $prop->getValue( $storage );
$parent_priority = -1;
$board_priority = -1;
// storage[added][actions][hook_name][] = [CallbackStringForm, priority, accepted_args]
$admin_menu_entries = $data['added']['actions']['admin_menu'] ?? array();
foreach ( $admin_menu_entries as $entry ) {
$cb_string = (string) ( $entry[0] ?? '' );
$priority = (int) ( $entry[1] ?? -1 );
if ( false !== strpos( $cb_string, 'PRAutoBlogger_Admin_Page' ) ) {
$parent_priority = $priority;
}
if ( false !== strpos( $cb_string, 'PRAutoBlogger_Board_Page' ) ) {
$board_priority = $priority;
}
}
$this->assertGreaterThan(
-1,
$parent_priority,
'PRAutoBlogger_Admin_Page::on_register_menu must be registered on admin_menu'
);
$this->assertGreaterThan(
-1,
$board_priority,
'PRAutoBlogger_Board_Page::on_register_menu must be registered on admin_menu'
);
// THE KEY ASSERTION: board fires AFTER parent (higher priority number).
$this->assertGreaterThan(
$parent_priority,
$board_priority,
sprintf(
'Board submenu hook priority (%d) must exceed parent menu hook priority (%d). '
. 'Both at 10 is the root cause of the 404: add_submenu_page fires before '
. 'add_menu_page, WP falls back to admin_page_* hookname, request-time '
. 'recompute finds nothing, wp_die 404.',
$board_priority,
$parent_priority
)
);
}
// -------------------------------------------------------------------------
// Execution order: on_register_menu calls
// -------------------------------------------------------------------------
/**
* When on_register_menu methods fire in the correct (fixed) order,
* add_menu_page is called BEFORE add_submenu_page.
*/
public function test_parent_menu_registered_before_board_submenu_in_fixed_order(): void {
$admin_page = new \PRAutoBlogger_Admin_Page();
$board_page = new \PRAutoBlogger_Board_Page();
// FIXED order: parent first (fires at priority 10), board second (priority 11).
$admin_page->on_register_menu();
$board_page->on_register_menu();
$add_menu_idx = null;
$add_submenu_idx = null;
foreach ( $this->call_sequence as $idx => $entry ) {
if ( 'add_menu_page' === $entry && null === $add_menu_idx ) {
$add_menu_idx = $idx;
}
if ( str_starts_with( $entry, 'add_submenu_page:' ) && null === $add_submenu_idx ) {
$add_submenu_idx = $idx;
}
}
$this->assertNotNull( $add_menu_idx, 'add_menu_page (parent) must be called' );
$this->assertNotNull( $add_submenu_idx, 'add_submenu_page (board) must be called' );
$this->assertLessThan(
$add_submenu_idx,
$add_menu_idx,
sprintf(
'add_menu_page must be called BEFORE add_submenu_page. '
. 'Call sequence: [%s].',
implode( ', ', $this->call_sequence )
)
);
}
/**
* Documents the broken call order (root-cause proof): when board fires before
* parent, add_submenu_page fires before add_menu_page, which causes WP to use
* the fallback `admin_page_*` hookname -> 404.
*/
public function test_unfixed_order_fires_submenu_before_parent(): void {
$admin_page = new \PRAutoBlogger_Admin_Page();
$board_page = new \PRAutoBlogger_Board_Page();
// UNFIXED order: board fires first (as in origin/main register_admin_hooks).
$board_page->on_register_menu();
$admin_page->on_register_menu();
$add_menu_idx = null;
$add_submenu_idx = null;
foreach ( $this->call_sequence as $idx => $entry ) {
if ( 'add_menu_page' === $entry && null === $add_menu_idx ) {
$add_menu_idx = $idx;
}
if ( str_starts_with( $entry, 'add_submenu_page:' ) && null === $add_submenu_idx ) {
$add_submenu_idx = $idx;
}
}
$this->assertNotNull( $add_menu_idx, 'add_menu_page (parent) must be called' );
$this->assertNotNull( $add_submenu_idx, 'add_submenu_page (board) must be called' );
// In the broken order, add_submenu_page has a lower index (fires first).
$this->assertLessThan(
$add_menu_idx,
$add_submenu_idx,
'Documents the broken state: add_submenu_page fires before add_menu_page when '
. 'board is invoked first. WP registers under admin_page_* -> 404.'
);
}
// -------------------------------------------------------------------------
// Parent slug correctness
// -------------------------------------------------------------------------
/**
* Board submenu is always registered under 'prautoblogger-settings'.
*/
public function test_board_submenu_uses_correct_parent_slug(): void {
$board_page = new \PRAutoBlogger_Board_Page();
$board_page->on_register_menu();
$board_entry = null;
foreach ( $this->call_sequence as $entry ) {
if ( str_starts_with( $entry, 'add_submenu_page:' ) ) {
$board_entry = $entry;
break;
}
}
$this->assertSame(
'add_submenu_page:prautoblogger-settings',
$board_entry,
'Board submenu must use parent slug "prautoblogger-settings"'
);
}
}