Current Milestone: M7 - Extensibility API (planned) Status: M0-M6 completed, M7-M9 planned
Goal: Complete scaffolding with green CI
- M0.1 - Complete directory structure
- M0.2 - Setup composer.json with dependencies
- M0.3 - PHPCS configuration (WPCS)
- M0.4 - Setup PHPUnit (config + bootstrap)
- M0.5 - GitHub Actions workflows
- M0.6 - Bootstrap files (main plugin + config)
- M0.7 - Core classes (Container, Plugin, Activator) - TDD
- M0.8 - Script bin/install-wp-tests.sh
Pattern Enforcement:
- ✅ Container uses
share()for shared instances, NOTsingleton() - ✅ Plugin receives Container via constructor, NO
get_instance() - ✅ Bootstrap function creates and configures, NO static factories
- ✅ No final classes, no final methods
Deliverable: Green CI with PHPCS + PHPUnit matrix + coverage 8.3
Goal: Base checks with working dashboard
- M1.1 - StorageInterface + CheckInterface (DI contracts)
- M1.2 - Storage service (Options API wrapper with
ops_health_prefix) - M1.3 - CheckRunner orchestrator (check execution + result saving)
- M1.4 - DatabaseCheck with constructor injection
$wpdb(TDD) - M1.5 - Scheduler service (WP-Cron every 15 minutes)
- M1.6 - Admin Menu registration
- M1.7 - HealthScreen rendering with capability check
- M1.8 - bootstrap.php with complete DI wiring
- M1.9 - Complete unit tests (104 tests, Brain\Monkey)
- M1.10 - Complete integration tests (33 tests, WP Test Suite)
- ✅ Activator: correct hook name
ops_health_run_checks, removedflush_rewrite_rules() - ✅ Activator: handles cron schedule/unschedule (not Plugin::init)
- ✅ DatabaseCheck:
$wpdbvia constructor injection (NO global) - ✅ DatabaseCheck: no db_host/db_name exposure in details
- ✅ DatabaseCheck: i18n messages with
__() - ✅ CheckRunner: try/catch for
\Throwableon each check - ✅ CheckRunner:
get_latest_results()type safety (always returns array) - ✅ Storage:
has()with sentinel object pattern - ✅ HealthScreen: defensive result key validation (isset)
- ✅ HealthScreen: "no checks" message when results are empty
- ✅ Plugin:
init()only registers hooks, does not schedule - ✅ bootstrap.php: injects global
$wpdbinto DatabaseCheck - ✅ composer.json: test script runs suites sequentially
- ✅ CI: coverage with separate files per suite
- ✅ Test: removed all
assertTrue(true)placeholders - ✅ Test: added tests for static properties
- ✅ Test: added tests for exceptions and edge cases
- ⏳ uninstall.php → planned for M6
Final Statistics:
- 11 source files in
src/ - 18 test files (11 unit + 7 integration)
- 137 total tests, 275 assertions
- PHPCS 100% clean (0 errors, 0 warnings)
Deliverable: the dashboard shows the Database check with WP-Cron auto-refresh ✅
Goal: Error log check with automatic sensitive data redaction
- M2.1 - RedactionInterface (DI contract for redaction)
- M2.2 - Redaction service (11 patterns: credentials, tokens, PII, paths)
- M2.3 - ErrorLogCheck with TDD (tail log, aggregation, redacted samples)
- M2.4 - DI wiring in bootstrap.php (RedactionInterface + ErrorLogCheck)
- M2.5 - Complete unit tests (56 new tests, Brain\Monkey + Mockery partial mock)
- M2.6 - Complete integration tests (8 new tests, WP Test Suite + temp files)
Redaction Service - 11 redaction patterns in ordered chain:
- Path WP_CONTENT_DIR ->
[WP_CONTENT](str_replace, most specific first) - Path ABSPATH ->
[ABSPATH]/(str_replace) - DB credentials (DB_PASSWORD, DB_USER, DB_NAME, DB_HOST) ->
[REDACTED] - WordPress salts (AUTH_KEY, SECURE_AUTH_KEY, etc.) ->
[REDACTED] - API key, secret, token ->
[REDACTED] - Bearer token ->
[REDACTED] - Password in URL and generic fields ->
[REDACTED] - Email ->
[EMAIL_REDACTED] - IPv4 ->
[IP_REDACTED](with octet validation 0-255) - IPv6 (min 5 groups to avoid false positives on timestamps) ->
[IP_REDACTED] - Home directory
/home/user->/home/[USER_REDACTED]
ErrorLogCheck - Secure error log summary:
- Path resolution:
WP_DEBUG_LOG(string) ->ini_get('error_log') - Validation: existence, readability, anti-symlink
- Efficient tail: max 512KB, max 100 lines,
flock(LOCK_SH)for concurrent access - Classification: fatal, parse, warning, notice, deprecated, strict, other
- Status: critical (fatal/parse > 0), warning (warning/deprecated/strict > 0), ok
- Max 5 critical/warning samples, redacted before inclusion
- Protected methods for testability with Mockery partial mock
- ✅ CheckRunnerInterface: new contract to decouple HealthScreen and Scheduler from concrete CheckRunner
- ✅ RedactionInterface in CheckRunner: exception messages redacted before inclusion in results
- ✅ RedactionInterface in DatabaseCheck:
$wpdb->last_errorredacted before inclusion in results - ✅ Container circular dependencies:
$resolvingarray detects infinite loops during resolution - ✅ Storage autoload=false:
update_option()with third parameterfalsefor large data - ✅ Scheduler self-healing admin-only:
is_admin()guard inregister_hooks()prevents self-healing on frontend - ✅ Scheduler constants:
HOOK_NAMEandINTERVALas class constants - ✅ Activator uses Scheduler constants:
Scheduler::HOOK_NAMEandScheduler::INTERVALinstead of hardcoded strings - ✅ HealthScreen CheckRunnerInterface: type-hint on interface, shows
result['name']withucfirst()fallback - ✅ ErrorLogCheck flock(LOCK_SH): shared lock during log reading for concurrent safety
- ✅ IPv4 regex octet validation: regex verifies each octet is 0-255 (not just \d{1,3})
- ✅ URL password restrictive regex: no whitespace in pattern to avoid false positives
- ✅ bootstrap.php container->instance():
$wpdbregistered withinstance()instead of closure - ✅ bootstrap.php CheckRunnerInterface binding: CheckRunner registered under
CheckRunnerInterface::class - ✅ CheckRunner includes 'name' in results: each result includes the
namekey from the check
Final Statistics:
- 15 source files in
src/ - 24 test files (15 unit + 9 integration)
- 210 total tests (169 unit + 41 integration), 472 assertions
- PHPCS 100% clean (0 errors, 0 warnings)
Deliverable: Dashboard shows Database + Error Log check with automatic redaction ✅
PHPStan Integration - Static analysis level 6:
- Installed
phpstan/phpstan+szepeviktor/phpstan-wordpress - Created
phpstan.neon(level 6,missingType.iterableValueignored) - Added
composer analysescript - Added PHPStan job in GitHub Actions CI (
.github/workflows/ci.yml) - Integrated in
bin/test-matrix.sh(executed with PHPCS) - Fix:
ErrorLogCheck::resolve_log_path()WP_DEBUG_LOG assignment to variable with@phpstan-ignorefor stub compatibility - Fix:
.phpcs.xml.distexclude.phpstan-cache - PHPStan level 6: 0 errors
Code Review 2 - 3 source fixes + 4 tests updated:
- Scheduler: self-healing with transient throttle (every hour) instead of
is_admin()guard - RedisCheck: unique smoke test key per run with
uniqid()(avoids race condition cron vs manual) - RedisCheck
cleanup_and_close(): accepts$smoke_keyparameter - Integration HealthScreenTest: corrected option key
ops_health_results→ops_health_latest_results - SchedulerTest: mocks
get_transient/set_transientinstead ofis_admin - RedisCheckTest:
Mockery::on()pattern matcher for dynamic key - Integration SchedulerTest:
delete_transientinstead ofset_current_screen - Total: 265 tests (212 unit + 53 integration), 620 assertions, PHPCS clean
- Lesson:
is_admin()limits self-healing to admin only; use transient throttle for frontend coverage - Lesson: shared Redis keys between concurrent executions cause race conditions; use
uniqid()for uniqueness
Code Review Post-M3 - 5 source fixes + 9 new tests + 6 tests updated:
- Activator.php:
900→15 * MINUTE_IN_SECONDS, added__()i18n - ErrorLogCheck.php:
classify_line()from 6 regex to single regex + map (reduces from 600 to 100 evaluations for 100 lines) - HealthScreen.php: extracted
exit→ protecteddo_exit()for testability - RedisCheck.php:
unset($e)→phpcs:ignore(2 catch blocks) - .gitignore: removed
composer.lock - +7 HealthScreen tests (complete process_actions), +2 DatabaseCheck, +1 Menu
- Updated 3 SchedulerTest + 3 ActivatorTest for new dependencies
- Total: 265 tests (212 unit + 53 integration), 617 assertions, PHPCS clean
M3 - Redis Check completed:
- Implemented
RedisCheckwith graceful degradation (Redis optional, all failures arewarning) - PHP Redis extension detection, connection, authentication, database selection, smoke test SET/GET/DEL
- Response time measurement (>100ms = warning)
- Host and errors redacted via
RedactionInterface - Protected methods (
is_extension_loaded,create_redis_instance,get_redis_config) for testability TestableRedisChecksubclass for integration test (pattern fromTestableErrorLogCheck)- Registered in
config/bootstrap.phpvia$runner->add_check() - 25 unit tests + 6 integration tests
- Total: 256 tests (203 unit + 53 integration), 572 assertions, PHPCS clean
install-wp-tests.sh version resolution fix:
grep -oreturned ALL WordPress versions from API (6.9.1, 6.9.0, 6.8.3, ...) instead of just the latestsvn exportreceived multiple arguments and failed withE205000: Error parsing arguments- Fix: added
head -1to take only the first (latest) version; removed deadgrepline - Lesson: WP version-check API returns multiple versions in JSON; always
head -1when extracting latest
Admin Action Buttons + ErrorLogCheck Fix:
- Added "Run Now" and "Clear Cache" buttons to admin dashboard page
CheckRunnerInterface::clear_results()new method for cache clearingHealthScreen::process_actions()handles POST with nonce + capability verificationMenu::add_menu()registersload-{$page_hook}hook for PRG pattern (avoids "headers already sent")- Fixed ErrorLogCheck: distinguishes "not configured" (warning) from "configured but file not yet created" (ok)
- Admin notice with auto-clearing transient for action feedback
- 225 tests (178 unit + 47 integration), 497 assertions, PHPCS clean
- Lesson:
wp_safe_redirect()must be called before any output; useload-{$page_hook}hook, not insiderender()callback - Lesson:
wp_nonce_field()echoes by default; Brain\Monkey mock must useechonotreturn
M2 Code Review (15/15 issues resolved):
- CheckRunnerInterface for decoupling (HealthScreen, Scheduler use interface)
- RedactionInterface injected in CheckRunner (redacts exceptions) and DatabaseCheck (redacts $wpdb errors)
- Container: circular dependency detection with
$resolvingarray - Storage:
autoload=falseinupdate_option() - Scheduler: self-healing only in admin context (
is_admin()guard), HOOK_NAME/INTERVAL constants - ErrorLogCheck:
flock(LOCK_SH)for safe concurrent access - Redaction: IPv4 regex with octet validation (0-255), restrictive URL password regex
- bootstrap.php:
container->instance($wpdb), CheckRunnerInterface binding - Activator: uses
Scheduler::HOOK_NAMEandScheduler::INTERVAL - CheckRunner: includes
namekey in results - 210 tests (169 unit + 41 integration), 472 assertions, PHPCS clean
- Lesson learned: WP test suite
is_admin()returns false; useset_current_screen('dashboard')to enable admin context in integration tests
Completed M1: Core Checks + Storage + Cron ✅
- Implemented StorageInterface and CheckInterface contracts
- Storage service with Options API, sentinel pattern in
has() - CheckRunner with try/catch resilience and type safety
- DatabaseCheck with
$wpdbconstructor injection, no info disclosure - Scheduler with 15-minute WP-Cron interval
- Admin Menu and HealthScreen with capability check and defensive rendering
- Complete code review: fixed 17/18 issues
- All 137 tests green, PHPCS 100% clean
- Added
bin/test-matrix.shfor local PHP 7.4-8.5 matrix testing (mirrors CI)
Post-M1 Hardening (code review Codex):
- Fixed
--parallelin test-matrix.sh: subshell results are now propagated via temporary files - Added Scheduler self-healing:
register_hooks()callsschedule()to re-create missing cron event - 137 tests (104 unit + 33 integration), 275 assertions
Post-M1 Improvements:
- Added autoloader guard in main plugin file (admin notice instead of fatal error if vendor/ missing)
- Created
bin/build-zip.shfor production ZIP generation (zipCLI with PHP ZipArchive fallback) - Fixed PHPCS doc comment SpacingAfter in 4 source files (Storage, Scheduler, DatabaseCheck, Menu)
- Added Version and PHPCS badges to README.md, updated license badge to GPL v3
- Added
dist/to .gitignore
Completed M0: Setup & Infrastructure ✅
- Created complete directory structure
- Setup composer.json with all dependencies (PHPUnit, WPCS, Brain\Monkey, etc.)
- Configured PHPCS for WordPress Coding Standards
- Setup PHPUnit configuration with coverage support
- Created GitHub Actions CI workflow (PHPCS + PHPUnit matrix PHP 7.4-8.5)
- Implemented core classes with TDD:
Containerclass - DI container with NO singleton patternPluginclass - Main orchestrator with constructor injectionActivatorclass - Activation/deactivation handler
- Created main plugin file and bootstrap function
Git Commits:
1b0944c- feat(M0): Complete setup & infrastructure with TDD4e2d182- docs: add comprehensive README and CONTRIBUTING guidesd7b3dd2- docs: traduci documentazione in italiano70a708e- docs: traduci commenti codice in italiano
CSS 'unknown' status: resolved in v0.6.0 withhealth-screen.css(class.ops-health-check-unknown)Action buttons inline style: resolved in v0.6.0 — migrated to dedicated CSShealth-screen.cssuninstall.php: resolved in v0.6.0- No known issues at this time.
Goal: Optional Redis check with graceful degradation
- M3.1 - RedisCheck with TDD (extension detection, connection, auth, smoke test, response time)
- ✅ Activator MINUTE_IN_SECONDS: uses
15 * MINUTE_IN_SECONDSand__()i18n for cron interval (aligned with Scheduler) - ✅ ErrorLogCheck classify_line(): optimized from 6 sequential regex to single regex with alternation + lookup map
- ✅ HealthScreen do_exit(): extracted
exitinto protected method for testability with Mockery partial mock - ✅ RedisCheck unset($e): removed anti-pattern, replaced with
phpcs:ignore - ✅ .gitignore composer.lock: removed entry (must be committed for reproducible builds)
- ✅ HealthScreenTest +7 tests: complete coverage of
process_actions()(early returns, run_now, clear_cache, notice) - ✅ DatabaseCheckTest +2 tests: warning on slow query, fallback
Unknown error - ✅ MenuTest +1 test: skip
load-hookwhenadd_menu_pagereturns false - ✅ SchedulerTest updated:
add_filterexpectation in 3register_hookstests - ✅ ActivatorTest updated:
MINUTE_IN_SECONDSdefinition and__()mock
Final Statistics:
- 16 source files in
src/ - 26 test files (16 unit + 10 integration)
- 314 total tests (215 unit + 99 integration), 744 assertions
- PHPCS 100% clean (0 errors, 0 warnings)
- PHPStan level 6: 0 errors
Deliverable: Dashboard shows Database + Error Log + Redis check with graceful degradation ✅
Code Review 3 - 4 source fixes + 4 tests improved + CI/config:
- CheckRunner: exception message internationalized with
__()for i18n - DatabaseCheck: 0.5s threshold extracted into
SLOW_QUERY_THRESHOLDconstant - ErrorLogCheck: defensive null coalesce
?? 'other'inclassify_line() - Activator: explanatory comment on duplicate
cron_schedulesfilter - HealthScreenTest:
$_POSTcleanup intearDown(), 4×assertTrue(true)→assertInstanceOf() - MenuTest: 2×
assertTrue(true)→assertInstanceOf() - ActivatorTest: 1×
assertTrue(true)→assertInstanceOf() - CheckRunnerTest: verifies
__()in exception test - CI: removed deprecated
--no-suggest, addedpermissions: contents: read - New
.gitattributeswithexport-ignorefor development files - build-zip.sh:
mkdir -pfor custom output directory - install-wp-tests.sh: quoted variables in critical paths
- Total: 314 tests (215 unit + 99 integration), 744 assertions, PHPCS + PHPStan clean
Test Matrix Fix - 3 issues resolved for test stability on PHP 7.4-8.5:
-
RedisCheckTest fatal without ext-redis:
extends \Redisin FakeRedis* helper classes causedClass 'Redis' not foundon PHP without ext-redis. Fix:extension_loaded('redis')guard onrequire_once+@requires extension redison test methods that use FakeRedis subclasses. The file-levelreturn;did not work because PHPUnit bypasses the guard during test discovery. -
DatabaseCheckTest flaky (EINTR): single
usleep()was interrupted by SIGALRM (PHPUnit php-invoker) causing duration < 0.5s despite usleep of 1.5-3s. Fix: busy-wait loop withusleep(50000)in increments that resumes after each interruption. -
test-matrix.sh count "?": the grep
'OK \(\K[0-9]+'did not match PHPUnit output when there are skipped tests (Tests: N, Assertions: M, Skipped: S.instead ofOK (N tests, M assertions)). Fix: grep fallback withTests: \K[0-9]+.
Improved test coverage: 265 → 314 tests (+49), 620 → 743 assertions (+123)
- Post-mortem documentation of Redis test matrix fix integrated into changelog and progress log
- Lesson:
usleep()can be interrupted by SIGALRM (EINTR); use busy-wait loop for timing tests - Lesson: PHPUnit changes output format with skipped tests; grep patterns must handle both formats
- Lesson:
@requires extension redison individual methods is the correct way to skip without ext-redis - Lesson:
extends \Redisis eager (parent must exist at class definition), but: \Redisreturn type is lazy
Goal: Multi-channel alerting on check status changes with anti-SSRF protection
- M4.1 - HttpClientInterface + HttpClient (anti-SSRF: scheme/port/IP validation, DNS resolution, no redirects)
- M4.2 - AlertChannelInterface + EmailChannel (
wp_mail(), configurable recipients) - M4.3 - AlertManagerInterface + AlertManager (state change detection, cooldown, dispatch, alert log)
- M4.4 - WebhookChannel (generic JSON POST, optional HMAC
X-OpsHealth-Signature) - M4.5 - SlackChannel (Block Kit payload, color-coded attachments)
- M4.6 - TelegramChannel (Bot API
sendMessage, HTML parse mode) - M4.7 - WhatsAppChannel (generic webhook, phone number, Bearer auth)
- M4.8 - Scheduler modification (optional
AlertManagerInterface, backward compatible) - M4.9 - AlertSettings admin page + Menu submenu (PRG, nonce, capability check)
- M4.10 - Bootstrap wiring + AlertingFlowTest (end-to-end integration)
HttpClient (Anti-SSRF):
is_safe_url(): scheme http/https only, ports 80/443 only, block private IPs (10/172.16/192.168/127/169.254/0.0.0.0)post():wp_remote_post()withredirection => 0, timeout 5s, DNS pinning viaCURLOPT_RESOLVE- Private
validate_and_resolve(): validates URL + resolves DNS → returns[host, ip, port] - Protected
create_dns_pin(): returns closure forhttp_api_curlaction (anti-TOCTOU) - Protected
resolve_host()wrapsgethostbyname()for testability (partial mock pattern) - Deps:
RedactionInterface
AlertManager (State Change Detection):
- Alert triggers: ok→warning, ok→critical, warning→critical, critical→warning, *→ok (recovery)
- No alert: same status (ok→ok, critical→critical, etc.)
- First run (empty previous): alert only if status ≠ ok
- Per-check cooldown via transient
ops_health_alert_cooldown_{check_id}(default 60 min /DEFAULT_COOLDOWN = 3600) - Recovery alerts (*→ok) bypass cooldown
- Alert log capped at
MAX_LOG_ENTRIES = 50entries - Storage keys:
alert_settings,alert_log - Deps:
StorageInterface,RedactionInterface
Notification Channels:
| Channel | Transport | Auth | Key Features |
|---|---|---|---|
wp_mail() |
N/A | Comma-separated recipients | |
| Webhook | HTTP POST JSON | HMAC SHA-256 | X-OpsHealth-Signature header |
| Slack | HTTP POST JSON | Webhook URL | Block Kit, color attachments |
| Telegram | Bot API | Bot token | HTML parse mode |
| HTTP POST JSON | Bearer token | Phone number field |
AlertSettings Admin Page:
- PRG pattern with nonce
ops_health_alert_settings, capabilitymanage_options - Per-channel enable/disable + credentials
- Global cooldown minutes setting (absint)
protected do_exit()for testability
Scheduler Integration:
- Optional
AlertManagerInterface $alert_manager = null(backward compatible) - Flow: read previous results →
run_all()→alert_manager->process($current, $previous) - "Run Now" button does NOT trigger alerts (alerts only on cron)
Settings Structure (stored as alert_settings via Storage):
[
'email' => ['enabled' => bool, 'recipients' => string],
'webhook' => ['enabled' => bool, 'url' => string, 'secret' => string],
'slack' => ['enabled' => bool, 'webhook_url' => string],
'telegram' => ['enabled' => bool, 'bot_token' => string, 'chat_id' => string],
'whatsapp' => ['enabled' => bool, 'webhook_url' => string, 'phone_number' => string, 'api_token' => string],
'cooldown_minutes' => int,
]Final Statistics:
- 27 source files in
src/(+11 from M3) - 47 test files (27 unit + 20 integration) (+21 from M3)
- 698 total tests (438 unit + 260 integration), 1548 assertions
- Unit coverage: 100% (1281/1281 lines, 136/136 methods, 20/20 classes)
- Integration coverage: 100% (1280/1280 lines, 136/136 methods, 20/20 classes)
- PHPCS 100% clean (0 errors, 0 warnings)
- PHPStan level 6: 0 errors
- +196 new tests for M4 + 10 tests added in code review 1 + 120 integration tests added in coverage push + 8 tests added in code review 2 + 5 tests added in 100% coverage push
Deliverable: Multi-channel alerting with anti-SSRF, DNS pinning, smart cooldown, admin configuration UI ✅
Code Review 2 Post-M4 - 5 findings resolved with strict TDD (RED → GREEN → REFACTOR):
High:
- HttpClient: DNS pinning via
CURLOPT_RESOLVE+http_api_curlaction (prevents TOCTOU/DNS rebinding between validation and HTTP request). Extractedvalidate_and_resolve()private +create_dns_pin()protected.
Medium:
- Scheduler:
catch (\Exception)→catch (\Throwable)inrun_checks()(catches TypeError, ValueError, not just Exception) - AlertManager: try/catch
\Throwableper-channel indispatch_to_channels()(channel isolation: one failing channel does not block the others)
Low:
- AlertSettings: password fields render
value=""+placeholder="********"(credentials never present in HTML/DOM source).build_settings_from_post()preserves existing secrets when POST field is empty. - EmailChannel:
empty($recipients)guard afterparse_recipients()insend()(preventswp_mail()call without recipients)
Total: 693 tests (437 unit + 256 integration), 1529 assertions, PHPCS + PHPStan clean
- Lesson:
CURLOPT_RESOLVEviahttp_api_curlWordPress hook is the standard solution for DNS pinning without breaking HTTPS/SNI - Lesson: password fields must use
value=""(never the real value) + preserve-on-empty to avoid losing the secret on save
Coverage push: 99.92%/98.98% → 100%/100% — 5 new tests to close the last uncovered lines:
Gaps identified via Clover XML parsing:
- AlertSettings
build_settings_from_post()line 327:$existing = []insideif (!is_array($existing))— corrupted storage - AlertSettings lines 336/340/344: preserve existing secrets when POST field is empty — not exercised in integration
- EmailChannel
send()lines 86-89:empty($recipients)guard — all recipients invalid afterparse_recipients() - AlertManager
dispatch_to_channels()lines 281-285:catch (\Throwable)— no channel was throwing exception in integration
Tests added:
- Unit AlertSettingsTest:
test_process_actions_handles_corrupted_existing_settings(storage returns non-array) - Integration AlertSettingsTest:
test_process_actions_preserves_existing_secrets_when_empty+test_process_actions_handles_corrupted_existing_settings - Integration EmailChannelTest:
test_send_returns_error_when_all_recipients_invalid - Integration AlertingFlowTest:
test_dispatch_catches_throwable_from_channel(ThrowingChannel + per-channel isolation)
Total: 698 tests (438 unit + 260 integration), 1548 assertions Coverage: 100% for both unit and integration, independently (1281/1281 + 1280/1280 lines) PHPCS + PHPStan clean
Code Review Post-M4 - 13 findings resolved (4 Critical, 3 High, 3 Medium, 3 Low):
Critical:
- HttpClient.post(): returns
success: falsefor non-2xx HTTP responses - AlertManager: cooldown set BEFORE dispatch (prevents alert spam in case of channel errors)
- TelegramChannel:
htmlspecialchars()on all interpolated variables in HTML messages - Scheduler:
try/catcharoundalert_manager->process()(cron resilience)
High:
- SlackChannel:
escape_mrkdwn()for*,_,~,`,&,<,> - EmailChannel:
is_email()inparse_recipients()filters invalid emails - AlertSettings:
type="password"+autocomplete="off"for tokens/secrets
Medium:
- AlertingFlowTest: strengthened with EmailChannel + meaningful assertions
- HttpClient: IPv6 rejection documented in
is_private_ip()PHPDoc - AlertManager:
STATUS_OK/WARNING/CRITICAL/UNKNOWNconstants replace hardcoded strings
Low:
- Interface tests:
assertTrue(interface_exists/method_exists)→ Reflection-based assertions - WebhookChannel:
X-OpsHealth-Signatureheader documented with verification instructions - WhatsAppChannel: E.164 phone validation (
is_valid_phone) inis_enabled()
Total: 556 tests (420 unit + 136 integration), 1285 assertions, PHPCS + PHPStan clean
Integration test coverage: 63.87% → 100% — 120 new integration tests:
7 new integration test files:
HttpClientTest(~25 tests): TestableHttpClient subclass, anti-SSRF validation,pre_http_requestinterception, tests with real HttpClient on direct IPsAlertSettingsTest(~16 tests): TestableAlertSettings subclass, PRG pattern, nonce/capability security, render with real settingsSlackChannelTest(~12 tests): Block Kit payload, color-coded attachments, recovery title, corrupted settingsTelegramChannelTest(~10 tests): Bot API URL, HTML parse mode, chat_id, corrupted settingsWebhookChannelTest(~12 tests): HMAC signature verification, no-secret header checkWhatsAppChannelTest(~13 tests): E.164 phone validation, Bearer auth headerEmailChannelTest(~12 tests):pre_wp_mailinterception,is_email()validation, wp_mail failure
3 enhanced integration test files:
MenuTest(+4 tests): submenu registration, render delegation, null AlertSettings, load hookSchedulerTest(+1 test): ThrowingAlertManager resilience (try/catch around process())AlertingFlowTest(+6 tests): real home_url/bloginfo, DEFAULT_COOLDOWN, missing status key, corrupted log, disabled channels, error redaction
Key techniques:
- TestableHttpClient: overrides
resolve_host()for controlled IP without DNS - TestableAlertSettings: overrides
do_exit()to prevent exit() in tests - ThrowingAlertManager: implements AlertManagerInterface, throws in process() for resilience testing
- Real HttpClient with direct IPs (127.0.0.1, 172.16.0.1, 192.168.1.1):
gethostbyname()on an IP returns the IP itself, resolves Xdebug coverage attribution limitation on subclass pre_http_requestfilter (2nd arg=$args, 3rd=$url) to interceptwp_remote_post()pre_wp_mailfilter: return true=intercept, return false=simulate failure- Admin user context:
self::factory()->user->create(['role'=>'administrator'])for$submenuglobal
Total: 685 tests (429 unit + 256 integration), 1497 assertions Coverage: 100% for both unit tests and integration tests, considered separately PHPCS + PHPStan clean
M4 - Alerting System completed in 10 sub-tasks:
- M4.1-M4.7: HttpClient + 5 channels + AlertManager (TDD, ~170 unit tests)
- M4.8: Scheduler modification with optional AlertManager injection
- M4.9: AlertSettings admin page + Menu submenu (+22 unit, +6 menu tests)
- M4.10: Bootstrap wiring + AlertingFlowTest (10 E2E integration tests)
Goal: DiskCheck, VersionsCheck, DashboardWidget + E2E testing with Playwright
- M5.1 - DiskCheck with TDD (configurable thresholds, protected wrappers, RedactionInterface)
- M5.2 - VersionsCheck with TDD (WP/PHP versions, update notifications, graceful fallback)
- M5.3 - DashboardWidget with TDD (worst-status, capability check, render with escaping)
- M5.4 - DiskCheck + VersionsCheck + DashboardWidget registration in bootstrap.php + Plugin.php
- M5.5 - E2E infrastructure (package.json, .wp-env.json, playwright.config.ts, tsconfig.json)
- M5.6 - E2E helpers (login.ts, selectors.ts) + bin/e2e-setup.sh
- M5.7 - E2E spec files (navigation, health-dashboard, alert-settings, dashboard-widget, security)
- M5.8 - CI integration (e2e job in ci.yml, .gitignore, .gitattributes)
- M5.9 - Unit + integration tests for DiskCheck, VersionsCheck, DashboardWidget
- M5.10 - All quality gates pass + 138 green E2E executions
DiskCheck:
WARNING_THRESHOLD = 20,CRITICAL_THRESHOLD = 10(percent free space)- Protected wrappers:
get_disk_path(),get_free_space(),get_total_space() - Edge cases: functions disabled →
is_enabled()false; functions return false → warning; total=0 → guard size_format((int) $free)cast for PHPStan compatibility- Deps:
RedactionInterface
VersionsCheck:
RECOMMENDED_PHP_VERSION = '8.3'- Status: core update → critical, plugin/theme update → warning, old PHP → warning
filter_real_updates(): keeps onlyresponse === 'upgrade'load_update_functions(): try/catch \Throwable,@codeCoverageIgnoreStart/Endaround require_once- No constructor dependencies (removed unused RedactionInterface after PHPStan flagged it)
DashboardWidget:
- Injects
CheckRunnerInterface determine_overall_status(): priority map (critical=3 > warning=2 > ok=1), empty=unknown- Capability check on both
add_widget()andrender() - Registered via
Plugin::init()→register_hooks()→wp_dashboard_setup
E2E Testing:
@wordpress/env^10.0.0 for Docker-based WordPress (WP 6.7, PHP 8.3)@playwright/test^1.49.0 with Chromium- 3 viewports: desktop (1280x720), tablet (768x1024), mobile (375x812)
- CI: desktop-only (46 tests, ~8 min), 1 worker, 60s timeout, login timeout 30s, job timeout 15 min, health check wait,
line+githubreporter - Locally: 1 worker, 30s timeout, 1 retry
bin/e2e-setup.sh: creates subscriber_e2e + editor_e2e test users- 5 spec files: navigation (6), health-dashboard (14), alert-settings (14), dashboard-widget (6), security (6)
Local Test Matrix (bin/test-matrix.sh):
- E2E integrated with lifecycle management (wp-env start/stop, test users, npm/docker prerequisites)
- Flags:
--e2e-only,--no-e2e,--tests-only,--phpcs-only,--parallel - Dot reporter for real-time progress, ANSI stripping for result parsing
- SKIP (yellow) when npm/Docker are not available
composer.jsonprocess-timeout: 0for long-running executions
Final Statistics (post code review + coverage push):
- 30 source files in
src/(+3 from M4) - 53 PHP test files (30 unit + 23 integration) (+6 from M4)
- 537 unit tests, 1203 assertions — 100% classes, methods, lines
- 296 integration tests, 598 assertions — 100% classes, methods, lines
- 46 E2E scenarios x 3 viewports = 138 test executions
- Local test matrix integrated with E2E (PHPCS + PHPStan + PHP 7.4-8.5 + E2E)
- PHPCS 100% clean, PHPStan level 6: 0 errors
Deliverable: Dashboard with 5 checks (Database, Error Log, Redis, Disk, Versions) + dashboard widget + complete E2E testing + local test matrix with E2E ✅
Goal: Prepare the plugin for WordPress.org submission.
Deliverable:
uninstall.phpwithUninstallerclass for complete data cleanup (options, cron, transients)readme.txtin WordPress.org standard format- ABSPATH guards on all source files + config
bin/build-zip.shupdated to include uninstall.php and readme.txttests/bootstrap.phpdefines ABSPATH for unit test compatibility
Final Statistics (post code review + multisite coverage):
- 31 source files in
src/, 2 CSS files inassets/css/ - 55 PHP test files (31 unit + 24 integration)
- 574 unit tests, 1336 assertions — 100% classes, methods, lines
- 322 integration tests, 655 assertions (single-site) / 684 assertions (multisite) — 100% combined
- 46 E2E scenarios x 3 viewports = 138 test executions
- PHPCS 100% clean, PHPStan level 6: 0 errors
Deliverable: WordPress.org ready plugin with uninstall.php, readme.txt, ABSPATH guards, HealthScreen UI with dedicated CSS ✅
Code Review Post-M6 - 4 improvements implemented from external review:
Fix:
- WebhookChannel HMAC: body serialized only once, HMAC signature computed on pre-serialized string, passed as string to HttpClient (avoids double serialization)
- HttpClientInterface
post(): acceptsarray|stringbody (PHPDoc@param array|string, no PHP type hint for 7.4 compatibility) - Uninstaller multisite:
uninstall()dispatches touninstall_network()(iterates all blogs) oruninstall_single()based onis_multisite() - uninstall.php: added
elseif(is_multisite())fallback with prefixed variables - build-zip.sh: removed
|| trueoncomposer install, added explicit check with exit 1 - readme.txt: clarified "46 scenarios; 3 viewports locally, desktop-only in CI"
Multisite Coverage Push:
tests/bootstrap.php: support forWP_TESTS_MULTISITEenv var →define('WP_TESTS_MULTISITE', true)- 3 new multisite integration tests in
UninstallerTest: multi-blog cleanup, cooldown transient, non-plugin data preservation composer.json: 3 new scripts (test:integration:multisite,test:coverage:multisite,test:coverageupdated with 3 runs)- Coverage: Uninstaller 81.25%→96.88% (multisite) / 81.25% (single-site) → 100% combined
New tests:
- +7 unit tests: WebhookChannel string body, HttpClient string/array body, Uninstaller multisite
- +4 integration tests: 3 multisite Uninstaller + 1 updated
Total: 574 unit (1336 assertions) + 322 integration (655/684 assertions), PHPCS + PHPStan clean
Lesson: is_multisite() branch requires two test runs (single-site + multisite); WP_TESTS_MULTISITE env var enables multisite mode in WP Test Suite bootstrap
Version: 0.7.0 Goal: Make the plugin extensible via standard WordPress hooks/filters + integrate with WordPress Site Health.
- M7.1 - Hook:
ops_health_register_checksinconfig/bootstrap.php(third-party check registration via$runner->add_check()) - M7.2 - Hook:
ops_health_register_channelsinconfig/bootstrap.php(third-party channel registration via$manager->add_channel()) - M7.3 - Filter:
ops_health_check_resultsinCheckRunner::run_all()(post-processing results before storage) - M7.4 - Action:
ops_health_checks_completedinCheckRunner::run_all()(react to results after storage) - M7.5 - Filter:
ops_health_alert_payloadinAlertManager::build_payload()(customize alert messages) - M7.6 - Action:
ops_health_alert_sentinAlertManager::dispatch_to_channels()(audit logging per channel) - M7.7 - Filter:
ops_health_cron_intervalinScheduler(configurable check frequency) - M7.8 - WordPress Site Health integration (
src/Admin/SiteHealthIntegration.php):site_status_tests+debug_informationfilters - M7.9 - Tests + documentation (unit ~45-55, integration ~20-25, pattern enforcement, wiki updates)
src/Admin/SiteHealthIntegration.phptests/Unit/Admin/SiteHealthIntegrationTest.phptests/Integration/Admin/SiteHealthIntegrationTest.php
config/bootstrap.php— 2do_actionhooks in share closuressrc/Services/CheckRunner.php— 1 filter + 1 actionsrc/Services/AlertManager.php— 1 filter + 1 actionsrc/Services/Scheduler.php— 1 filtersrc/Core/Plugin.php— register SiteHealthIntegration
| Hook | Type | Location | Purpose |
|---|---|---|---|
ops_health_register_checks |
action | config/bootstrap.php |
Register custom checks |
ops_health_register_channels |
action | config/bootstrap.php |
Register custom alert channels |
ops_health_check_results |
filter | CheckRunner::run_all() |
Modify results before storage |
ops_health_checks_completed |
action | CheckRunner::run_all() |
React after checks complete |
ops_health_alert_payload |
filter | AlertManager::build_payload() |
Customize alert content |
ops_health_alert_sent |
action | AlertManager::dispatch_to_channels() |
Per-channel audit logging |
ops_health_cron_interval |
filter | Scheduler |
Configure check frequency |
Deliverable: Fully extensible plugin with 7 hooks/filters + WordPress Site Health integration
Version: 0.8.0 Goal: Expose plugin data via REST API, add downloadable JSON export, and store check history.
- M8.1 -
ExportServiceInterfacecontract (src/Interfaces/ExportServiceInterface.php) - M8.2 -
ExportServiceimplementation with redaction (src/Services/ExportService.php) - M8.3 -
RestControllerwith 4 endpoints (src/Api/RestController.php) - M8.4 - Check history in
CheckRunner(append toops_health_results_history, capped at 24, filterable limit) - M8.5 - Admin UI: "Export JSON" button in
HealthScreen - M8.6 - Container wiring for ExportService + RestController in
config/bootstrap.php - M8.7 -
CheckRunnerInterface: addget_history(): array - M8.8 -
Uninstaller: addops_health_results_historyto cleanup - M8.9 - Tests + documentation (unit ~50-60, integration ~25-30, E2E ~5-8, wiki updates)
| Method | Endpoint | Description | Auth |
|---|---|---|---|
| GET | /wp-json/ops-health/v1/status |
Latest check results (cached) | manage_options |
| POST | /wp-json/ops-health/v1/run |
Trigger check run (rate-limited) | manage_options |
| GET | /wp-json/ops-health/v1/export |
Full diagnostic JSON (redacted) | manage_options |
| GET | /wp-json/ops-health/v1/history |
Check history (last 24 runs) | manage_options |
src/Interfaces/ExportServiceInterface.phpsrc/Services/ExportService.phpsrc/Api/RestController.php- Corresponding unit and integration test files
config/bootstrap.php— wire ExportService + RestControllersrc/Services/CheckRunner.php— history tracking +get_history()src/Interfaces/CheckRunnerInterface.php— addget_history()src/Admin/HealthScreen.php— Export JSON buttonsrc/Core/Uninstaller.php— cleanupops_health_results_historysrc/Core/Plugin.php— register REST routes viarest_api_init
Deliverable: REST API with 4 endpoints, JSON diagnostic export, check history with 24-entry rolling window
Version: 0.9.0 Goal: Full WP-CLI interface for headless/DevOps use, with monitoring-compatible exit codes.
- M9.1 -
HealthCommandclass (src/Cli/HealthCommand.php) with DI,WP_CLIguard - M9.2 - Subcommand:
wp ops-health status(table/json/csv, exit codes 0/1/2) - M9.3 - Subcommand:
wp ops-health run(fresh check,--quietmode) - M9.4 - Subcommand:
wp ops-health export(JSON to stdout or--output=<file>) - M9.5 - Subcommand:
wp ops-health list-checks(registered checks with status) - M9.6 -
CheckRunnerInterface: addget_checks(): array - M9.7 - Container wiring for HealthCommand with
WP_CLIguard - M9.8 - Tests + documentation (unit ~35-40, integration ~15-20, E2E ~8-10, wiki updates)
| Command | Description | Exit Codes |
|---|---|---|
wp ops-health status |
Show latest cached results | 0=ok, 1=warning, 2=critical |
wp ops-health run |
Trigger fresh check run | 0=ok, 1=warning, 2=critical |
wp ops-health export |
JSON diagnostic export | 0=success, 1=error |
wp ops-health list-checks |
List registered checks | 0=success |
All commands support --format=json|table|csv (WP-CLI standard). run supports --quiet for monitoring scripts. export supports --output=<file>.
Exit codes are compatible with Nagios/Icinga/Zabbix monitoring systems.
src/Cli/HealthCommand.php- Corresponding unit and integration test files
config/bootstrap.php— wire HealthCommand withWP_CLIguardsrc/Interfaces/CheckRunnerInterface.php— addget_checks()src/Services/CheckRunner.php— implementget_checks()src/Core/Plugin.php— register CLI command
Deliverable: Full WP-CLI interface with 4 subcommands, monitoring-compatible exit codes, pipe-friendly JSON export
| Milestone | Version | Status | Source Files | Test Files | Tests |
|---|---|---|---|---|---|
| M0 | 0.1.0 | ✅ | 7 | 7 | ~40 |
| M1 | 0.1.0 | ✅ | 11 | 18 | 137 |
| M2 | 0.2.0 | ✅ | 15 | 24 | 210 |
| M3 | 0.3.0 | ✅ | 16 | 26 | 314 |
| M4 | 0.4.0 | ✅ | 27 | 47 | 698 |
| M5 | 0.5.0 | ✅ | 30 | 53 | 833+46 E2E |
| M6 | 0.6.2 | ✅ | 31 | 55 | 896+46 E2E |
| M7 | 0.7.0 | ⏳ | +1 | +2 | +65-80 |
| M8 | 0.8.0 | ⏳ | +3 | +4 | +80-98 |
| M9 | 0.9.0 | ⏳ | +1 | +2 | +58-70 |
Post-M9 projection: ~36 source files, ~63 test files, ~1100+ PHP tests + ~60 E2E scenarios