diff --git a/AGENTS.md b/AGENTS.md index 7b5c5a1..da95385 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -172,18 +172,21 @@ All user-facing strings must be translatable. When writing or refactoring PHPUnit tests: -- Keep production code aligned with real WordPress runtime expectations. -- Do not add test-only `function_exists()` guards for core WordPress APIs just to satisfy PHPUnit. -- If a test environment is missing a WordPress function, add a deterministic stub in `tests/Support/WordPressStubs.php`. +- Never add test-only logic, boot contracts, or test-oriented exceptions/messages to plugin runtime code in `includes/` or `clawpress.php`. +- Do not introduce production `assert_*` helpers, runtime throws, or `function_exists()` guards solely to make tests pass. +- If PHPUnit needs a missing WordPress API, add a deterministic stub in `tests/Support/WordPressStubs.php` instead of changing plugin behavior. - When a stub needs state, store it in `WordPress_Stubs` and reset it in `WordPress_Stubs::reset()` so tests remain isolated. - Keep stubs minimal and behavior-focused: enough for assertions without recreating WordPress internals. -- If behavior is truly optional in production (version-gated or feature-detected APIs), keep runtime guards and prefer covering both paths in tests. +- Tests must validate real plugin behavior and observable outcomes, not internal test scaffolding. +- Prefer exercising registered hooks with `do_action()`/`apply_filters()` and asserting side effects (registered menus/routes, enqueued assets, persisted meta/options, scheduler calls). +- Avoid low-value tests that only assert stub presence (for example `function_exists()` contract lists) or only test helper assertions without behavior coverage. +- If behavior is truly optional in production (version-gated or feature-detected APIs), keep runtime guards and cover both paths in tests. ### Verification Expectations - Run targeted PHPUnit tests for the changed module first. - Run the full PHPUnit suite when shared test support files (like `WordPressStubs.php`) are changed. -- Prefer assertions that verify integration calls happened (for example submenu registration/removal and metadata checks). +- Prefer assertions that verify integration calls and state transitions happened (for example submenu registration/removal and metadata checks). ## Important Notes diff --git a/includes/class-plugin.php b/includes/class-plugin.php index 85ccae2..e398fc0 100644 --- a/includes/class-plugin.php +++ b/includes/class-plugin.php @@ -45,7 +45,6 @@ private function __construct() { // Initialize AI client. Goto Settings -> AI Credentials to set up. add_action( 'init', [ 'WordPress\AI_Client\AI_Client', 'init' ] ); } - /** * Get singleton instance. */ diff --git a/tests/Unit/ActionLogHelperTest.php b/tests/Unit/ActionLogHelperTest.php index e3c385c..0662076 100644 --- a/tests/Unit/ActionLogHelperTest.php +++ b/tests/Unit/ActionLogHelperTest.php @@ -31,6 +31,7 @@ function dbDelta( string $queries ): array { use ClawPress\Helpers\Action_Log_Helper; use ClawPress\Plugin; use ClawPress\Tests\Support\TestCase; +use ClawPress\Tests\Support\WordPress_Stubs; /** * Minimal wpdb stub for action log helper tests. @@ -146,6 +147,56 @@ public function test_plugin_activation_creates_action_log_table(): void { $this->assertStringContainsString( 'clawpress_action_logs', (string) $GLOBALS['clawpress_test_dbdelta_queries'][0] ); } + public function test_plugin_activation_initializes_default_panel_state_for_current_user_when_missing(): void { + WordPress_Stubs::$current_user_id = 27; + + Plugin::activate(); + + $this->assertArrayHasKey( 27, WordPress_Stubs::$user_meta ); + $this->assertArrayHasKey( 'clawpress_panel_state', WordPress_Stubs::$user_meta[27] ); + $this->assertSame( + [ + 'open' => true, + 'width' => 420, + 'last_history_id' => '', + 'welcome_card_seen' => false, + ], + WordPress_Stubs::$user_meta[27]['clawpress_panel_state'] + ); + } + + public function test_plugin_activation_does_not_overwrite_existing_panel_state(): void { + WordPress_Stubs::$current_user_id = 48; + WordPress_Stubs::$user_meta[48] = [ + 'clawpress_panel_state' => [ + 'open' => false, + 'width' => 600, + 'last_history_id' => 'history-2', + 'welcome_card_seen' => true, + ], + ]; + + Plugin::activate(); + + $this->assertSame( + [ + 'open' => false, + 'width' => 600, + 'last_history_id' => 'history-2', + 'welcome_card_seen' => true, + ], + WordPress_Stubs::$user_meta[48]['clawpress_panel_state'] + ); + } + + public function test_plugin_activation_skips_panel_state_when_no_authenticated_user(): void { + WordPress_Stubs::$current_user_id = 0; + + Plugin::activate(); + + $this->assertArrayNotHasKey( 0, WordPress_Stubs::$user_meta ); + } + public function test_log_event_persists_row_into_action_log_table(): void { $helper = Action_Log_Helper::get_instance(); $result = $helper->log_event( diff --git a/tests/Unit/AdminPageTest.php b/tests/Unit/AdminPageTest.php index aefbc18..1a1c967 100644 --- a/tests/Unit/AdminPageTest.php +++ b/tests/Unit/AdminPageTest.php @@ -10,6 +10,7 @@ namespace ClawPress\Tests\Unit; use ClawPress\AdminPage\Admin_Page; +use ClawPress\PostTypes\Post_Types; use ClawPress\Tests\Support\TestCase; use ClawPress\Tests\Support\WordPress_Stubs; @@ -26,11 +27,17 @@ public function test_register_adds_expected_hooks(): void { } public function test_register_admin_page_registers_menu_item(): void { - $admin_page = new Admin_Page(); - $admin_page->register_admin_page(); + new Admin_Page(); + + do_action( 'admin_menu' ); $this->assertCount( 1, WordPress_Stubs::$menu_pages ); $this->assertSame( 'clawpress', WordPress_Stubs::$menu_pages[0]['menu_slug'] ); + $this->assertCount( 1, WordPress_Stubs::$removed_submenu_pages ); + $this->assertCount( 3, WordPress_Stubs::$submenu_pages ); + $this->assertSame( 'clawpress', WordPress_Stubs::$submenu_pages[0]['menu_slug'] ); + $this->assertSame( 'edit.php?post_type=' . Post_Types::AGENT_FILE_POST_TYPE, WordPress_Stubs::$submenu_pages[1]['menu_slug'] ); + $this->assertSame( 'edit.php?post_type=' . Post_Types::AGENT_MEMORY_POST_TYPE, WordPress_Stubs::$submenu_pages[2]['menu_slug'] ); } public function test_render_admin_page_outputs_mount_node(): void { @@ -43,16 +50,18 @@ public function test_render_admin_page_outputs_mount_node(): void { } public function test_enqueue_admin_assets_bails_for_unrelated_screen(): void { - $admin_page = new Admin_Page(); - $admin_page->enqueue_admin_assets( 'dashboard_page' ); + new Admin_Page(); + + do_action( 'admin_enqueue_scripts', 'dashboard_page' ); $this->assertCount( 0, WordPress_Stubs::$enqueued_scripts ); $this->assertCount( 0, WordPress_Stubs::$enqueued_styles ); } public function test_enqueue_admin_assets_enqueues_script_and_style(): void { - $admin_page = new Admin_Page(); - $admin_page->enqueue_admin_assets( 'toplevel_page_clawpress' ); + new Admin_Page(); + + do_action( 'admin_enqueue_scripts', 'toplevel_page_clawpress' ); $this->assertCount( 1, WordPress_Stubs::$enqueued_scripts ); $this->assertCount( 1, WordPress_Stubs::$enqueued_styles ); diff --git a/tests/Unit/PluginTest.php b/tests/Unit/PluginTest.php index 81f8b1b..a4c0a36 100644 --- a/tests/Unit/PluginTest.php +++ b/tests/Unit/PluginTest.php @@ -10,10 +10,19 @@ namespace ClawPress\Tests\Unit; use ClawPress\Plugin; +use ClawPress\PostTypes\Post_Types; use ClawPress\Tests\Support\TestCase; use ClawPress\Tests\Support\WordPress_Stubs; final class PluginTest extends TestCase { + protected function setUp(): void { + parent::setUp(); + + $instance_property = new \ReflectionProperty( Plugin::class, 'instance' ); + $instance_property->setAccessible( true ); + $instance_property->setValue( null, null ); + } + public function test_get_instance_wires_all_module_hooks_once(): void { $instance = Plugin::get_instance(); $this->assertSame( $instance, Plugin::get_instance() ); @@ -34,4 +43,38 @@ public function test_get_instance_wires_all_module_hooks_once(): void { $this->assertContains( 'action_scheduler_ensure_recurring_actions', $hooks ); $this->assertContains( 'clawpress_heartbeat_tick', $hooks ); } + + public function test_plugin_boot_admin_menu_hook_registers_expected_menu_items(): void { + Plugin::get_instance(); + + do_action( 'admin_menu' ); + + $this->assertCount( 1, WordPress_Stubs::$menu_pages ); + $this->assertSame( 'clawpress', WordPress_Stubs::$menu_pages[0]['menu_slug'] ); + $this->assertCount( 1, WordPress_Stubs::$removed_submenu_pages ); + $this->assertCount( 3, WordPress_Stubs::$submenu_pages ); + $this->assertSame( 'clawpress', WordPress_Stubs::$submenu_pages[0]['menu_slug'] ); + $this->assertSame( 'edit.php?post_type=' . Post_Types::AGENT_FILE_POST_TYPE, WordPress_Stubs::$submenu_pages[1]['menu_slug'] ); + $this->assertSame( 'edit.php?post_type=' . Post_Types::AGENT_MEMORY_POST_TYPE, WordPress_Stubs::$submenu_pages[2]['menu_slug'] ); + } + + public function test_plugin_boot_admin_enqueue_scripts_hook_enqueues_admin_and_panel_assets(): void { + Plugin::get_instance(); + + do_action( 'admin_enqueue_scripts', 'toplevel_page_clawpress' ); + + $enqueued_script_handles = array_column( WordPress_Stubs::$enqueued_scripts, 'handle' ); + $enqueued_style_handles = array_column( WordPress_Stubs::$enqueued_styles, 'handle' ); + $localized_script_names = array_column( WordPress_Stubs::$localized_scripts, 'object_name' ); + + $this->assertCount( 2, WordPress_Stubs::$enqueued_scripts ); + $this->assertCount( 2, WordPress_Stubs::$enqueued_styles ); + $this->assertCount( 2, WordPress_Stubs::$localized_scripts ); + $this->assertContains( 'clawpress', $enqueued_script_handles ); + $this->assertContains( 'clawpress-panel', $enqueued_script_handles ); + $this->assertContains( 'clawpress', $enqueued_style_handles ); + $this->assertContains( 'clawpress-panel', $enqueued_style_handles ); + $this->assertContains( 'CLAWPRESS_ADMIN', $localized_script_names ); + $this->assertContains( 'CLAWPRESS_PANEL', $localized_script_names ); + } }