- Single 'Current totals' table at top covering all 287 tests + 486 mutants - All inline deferred items replaced with ↳ pointer to Phase 14 - Phase 14 organized by layer: unit, integration, HTTP, browser, CI, quality - Removed duplicate 'Current totals' tables scattered through doc Signed-off-by: Chuck Adams <chaz@chaz.works>
14 KiB
Implementation Order
The plan is designed to be executed incrementally. Each phase produces runnable tests before moving on.
Current totals
| Layer | Count | Assertions | Key Metric | Runner |
|---|---|---|---|---|
| Unit | 248 | 430 | 1 skipped | composer run test:unit (local) |
| Integration | 12 | 38 | 2 skipped | bin/run-integration.sh (Docker) |
| HTTP | 12 | 38 | all pass | bin/run-integration.sh (Docker) |
| Browser | 15 | — | all pass | npm run test:browser (Playwright) |
| Mutation | 486 mutants | — | 97% covered MSI | composer run infection (Docker) |
| Total | 287 tests / 486 mutants | 506+ |
Phase 1: Infrastructure migration ✅
- Rename
tests/phpunit/→tests/unit/ - Move
phpunit.xml.dist→tests/unit/phpunit.xml, adjust paths - Move
tests/phpunit/multisite.xml→tests/unit/multisite.xml - Move
tests/phpunit/tests/Packages/*→tests/unit/tests/Packages/*(preserve existing tests) - Move
tests/phpunit/tests/SampleTest.php→tests/unit/tests/SampleTest.php - Update
composer.jsonscripts:test→test:unit, pointing attests/unit/phpunit.xml. Alsotest:multisite→test:unit:multisite. Updatedcoverage:*paths. - Update
package.jsonnpm scripts to reference new composer script names - Update
tests/unit/README.mdwith new paths - Bump
.wp-env.jsonPHP version from 7.4 → 8.0 (hard floor per AGENTS.md) - Add
"platform": {"php": "8.0"}tocomposer.jsonconfig → rancomposer update→ lock file resolves all deps for PHP 8.0 - Fix
test:php:install-depsnpm script: add--ignore-platform-req=ext-gmp(wp-env PHP 8.0 image lacks gmp) - Verify existing tests still pass: 19 tests, 30 assertions ✅
Remaining known issue: simplito/elliptic-php (dependency of fairpm/did-manager) requires ext-gmp, which is missing from the wp-env PHP 8.0 Docker image. Workaround: --ignore-platform-req=ext-gmp in the install-deps script. A proper fix would be installing gmp in the Docker image or upstreaming an ext-gmp change to the wp-env image.
Phase 2: Fixtures & factories ✅
- Create
tests/fixtures/directory - Create all fixture JSON files (did-doc, metadata-doc, release-doc variants — 13 files total)
- Create
tests/Factory/MetadataDocumentFactory.php(full, minimal, from_fixture, builder, error paths) - Create
tests/Factory/ReleaseDocumentFactory.php(full, with_version, with_requirements, builder, error paths) - Add
autoload-devPSR-4 entry forFAIR\Tests\→tests/in composer.json - Verify existing tests still pass: 19 tests, 30 assertions ✅
Phase 3: DID pipeline pure functions (Priority A) ✅
GetDidHashTest.php— 8 tests: hex string, deterministic, different DIDs, error propagation, empty string, 32-char, long DID, non-plc methodGetLanguagePriorityListTest.php— 11 tests: full locale first, underscore conversion, lowercase, prefix decomposition, x- subtag skip, doubled primary code, defaults, simple locale, filter hook, filter override, zh-Hans-CN orderPickArtifactByLangTest.php— 10 tests: exact match, specificity preference, no-match fallback, empty artifacts, single artifact, doubled primary code, en-us default, filter hook, filter override, underscore localePickReleaseTest.php— 7 tests: latest default, specific version, version not found, sort correctness, single release, empty releases, null versionVersionRequirementsTest.php— 9 tests: requires_php, requires_wp, tested_to, all three, empty, caret/tilde strip, non-env ignore, missing requires, missing suggestsGetUnmetRequirementsTest.php— 6 tests (+ data provider): all met, unmet PHP, unmet WP, empty, multi-unmet joined, invalid specifiers, unknown env, operator comparisonCheckRequirementsTest.php— 4 tests: all met, PHP unmet, WP unmet, empty requiresGetIconsTest.php— 7 tests: 1x/2x, wporg SVG default, non-wporg SVG, empty, no valid sizes, only 1x, only 2xGetBannersTest.php— 5 tests: low/high, empty, no valid sizes, only low, only highGetHashedFilenameTest.php— 5 tests: plugin slug with hash, theme slug, no double-append, deterministic, different DIDsValidatePackageAliasTest.php— 9 tests (cached + uncached): cache hit, cache set, unique keys, no aliases, non-fair aliases, multiple aliases, invalid domain, no TLD, excessively long domain, missing alsoKnownAs, non-string aliases
Bug fixed: pick_release() — added empty-array guard to prevent TypeError on reset() returning false with ?ReleaseDocument return type.
Note: get_site_transient converts null → ''; alias cache test accounts for this.
Phase 4: DTO validation ✅
MetadataDocumentTest.php— 13 tests (from_data): all fields, minimal, 5 missing mandatory fields, missing releases, invalid release propagation, optional fields null, multiple releases parsed, security array. 5 tests (from_response): valid response, invalid JSON, valid JSON + invalid data, empty body, null body (TypeError note)ReleaseDocumentTest.php— 10 tests: all fields, with requirements, specific version, missing version, missing artifacts, optional fields null, minimal artifacts, builder with unset fields
Phase 5: DID pipeline transient/HTTP functions (Priority B) ✅
CacheUpdateErrorTest.php— 7 tests: cache error, timestamp, lifetime, clear, idempotent, DID isolationGetDidDocumentTest.php— 3 tests: cache hit, cached error, parse error+cacheFetchMetadataDocTest.php— 6 tests: cache hit, HTTP failure caching, non-200, cache on success, metadata from HTTPFetchPackageMetadataTest.php— 6 tests: no service, ID mismatch, success, DID error propagation + get_latest_release_from_did (2 tests: success, no keys)PipelineWPTest.php— 20 tests: add_package_to_release_cache (4), maybe_add_accept_header (5), search_by_did (6), get_plugin_information (3)
Uses pre_http_request filter + pre-seeded transients instead of real HTTP calls.
Phase 6: Updater unit tests ✅
UpdaterTest.php— 14 tests: register/get plugins (3), register/get themes (2), unknown DID (2), overwrite, get_plugins/get_themes, empty, independent plugin/theme, get_plugin_by_file, unknown file + 8 tests forshould_run_on_current_page(plugins, themes, update-core, update, plugin-install, admin-ajax, edit.php, post.php)PackageTest.php— 8 tests: PluginPackage construct+version, slug, relative path, deep nesting, version override + ThemePackage construct+version, slug, type distinctionGetTrustedKeysTest.php— 5 tests: no cached DID, fetch failure, no keys, empty verificationMethod, non-fair filteringDisplayPluginUpdateErrorTest.php— 6 tests: no error, non-error transient, error row output, active class, HTML sanitization, colspanGetPackagesTest.php— 4 tests: Plugin ID header, multiple plugins, no header, keys structure
Phase 6b: Updater edge cases ✅
GetTrustedKeysTest.php— included aboveDisplayPluginUpdateErrorTest.php— included aboveGetPackagesTest.php— included above- ↳
SignatureVerificationTest.php,RegisterPluginRowHooksTest.php→ Phase 14
Uses temp file creation (wp_mkdir_p + file_put_contents) so constructors'
get_file_data() calls resolve successfully.
Phase 7: Supplementary module unit tests ✅
AvatarsTest.php— 18 tests: should_replace_url (5), generate_default_avatar (8; 1 skipped for color hook bug), get_avatar_alt (4)PingsTest.php— 11 tests: remove_pingomatic (5), get_indexnow_key (4), register_query_vars (2)SaltsTest.php— 9 tests: replace_salt_api (3), define_keynames (1), generate_salt (3), response_body (2), get_response (1)DefaultRepoAndVersionCheckTest.php— 6 tests: default repo domain (2), version-check constants (4) ↳Compatibility/PolyfillTest.php,Settings/*,Upgrades/*→ Phase 14
Bugs found: generate_default_avatar() uses add_filter() instead of apply_filters() for fair_avatars_default_color (1 test skipped). esc_attr() can expand salt strings beyond 64 chars.
Phase 8: Integration harness ✅
tests/sites/ephemeral/integration/docker-compose.yml— self-contained WP 6.4 + PHP 8.0 + MariaDB + wp-cli + mock-servertests/sites/ephemeral/Dockerfile.wp— custom WP imagetests/mock-server/— Dockerfile + PHP built-in server emulating PLC Directory and FAIR Repository APIstests/mock-server/index.php— file-based request log, fixture-driven responsestests/integration/bootstrap.php— loads WP directly (no WP_UnitTestCase needed)tests/integration/phpunit.xml— PHPUnit configtests/sites/ephemeral/integration/seed.php— registers test plugin with DID headerbin/run-integration.sh— full lifecycle with trap EXIT teardown guaranteeFAIR_PLC_DIRECTORY_URLconstant (minimal production change for testability)
Phase 9: Integration tests ✅
DidResolutionIntegrationTest.php— 4 tests: mock health, full pipeline, log check (skipped), unknown DID errorPackageDataIntegrationTest.php— 5 tests: complete response, _fair metadata, no-service error, unknown DID, cachingUpdateTransientIntegrationTest.php— 3 tests: valid transient, seeded plugin (skipped), empty registry ↳SignatureVerificationIntegrationTest.php,WpCliCompatTest.php→ Phase 14
12 integration tests (10 pass, 2 skipped), 38 assertions.
Phase 10: HTTP test harness & tests ✅
tests/http/bootstrap.php— loads WP + plugin with admin includestests/http/phpunit.xml— PHPUnit configSaltApiHttpTest.php— 3 tests: salt API URL interception, 64-char values, passthroughDefaultRepoHttpTest.php— 5 tests: domain config, non-WP.org passthrough, filter registration, plugins/themes API interceptionAvatarHttpTest.php— 4 tests: should_replace_url, SVG default avatar, alt text for users ↳AdminAjaxTest.php,PluginsApiTest.php,UpdateTransientShapeTest.php→ Phase 14
12 HTTP tests + 12 integration = 24 Docker tests, 76 assertions.
Phase 11: Browser test harness & tests ✅
tests/browser/package.json— isolated @playwright/test depstests/browser/playwright.config.ts— chromium, auth state, CI retriestests/browser/global-setup.ts— login as browser_admin, save storagetests/browser/specs/direct-install.spec.ts— 9 tests: label, pattern, required, submit focus, validation, thickbox role/label/iframe/close (all pass)tests/browser/specs/search-did.spec.ts— 6 tests: searchbox label, DID result, no-results, install button, hostname, heading (all pass) ↳install-activate-update.spec.ts,avatar-upload.spec.ts,update-error-row.spec.ts→ Phase 14
15 tests pass, 0 skipped, 0 failed.
Run: npm run test:browser (needs npm run test:browser:docker:start first)
Phase 12: CI workflow ✅
.github/workflows/integration-tests.yml— new workflow (complements existingphpunit-tests.yml)docker-testsjob: PHP 8.0/8.4 × WP 5.4/latest matrix, runsbin/run-integration.sh(integration + HTTP)browser-testsjob: Playwright fast tests, runs on PRs only when labeledrun-browser-tests, always on push to main/dev/releasebrowser-slowjob:@slowtests (install/activate/update flow), push to main/release only- All Docker jobs use trap-based teardown + explicit cleanup verification ↳ Coverage reporting, Slack/Discord notification → Phase 14
Existing phpunit-tests.yml (PHP matrix unit tests) and coding-standards.yml (PHPCS + PHPStan) are preserved unchanged.
Phase 13: Coverage baseline & Infection ✅
infection.json— source ininc/, excludes admin/wp-cli/compatibility/settings/upgradestests/sites/ephemeral/mutation/Dockerfile— PHP 8.5-cli-alpine + mysqli + xdebugtests/sites/ephemeral/mutation/docker-compose.yml— MySQL 8.0 + mutation containertests/infection-bootstrap.php— FAIR autoloader + WordPress class/function stubscomposer run infection— runs mutation testing in Docker
Results: 486 mutations, 45 killed, 440 uncovered, 1 escaped, 0 errors, 0 timeouts
- Covered Code MSI: 97% (excellent test quality for covered code)
- Overall MSI: 9% (rooms for improvement in uncovered modules)
- Escaped mutant:
ArrayItemRemovalonReleaseDocument::$optional— minor gap
Phase 14: Deferred & future work
Unit tests
SignatureVerificationTest.php— needs real crypto operations, better suited for manual/integration testingRegisterPluginRowHooksTest.php— hook registration verified implicitly through admin_init flowCompatibility/PolyfillTest.php— low priority, polyfills are self-evidentSettings/*— heavily WP-hook dependent, needs WP test suite stub improvementsUpgrades/*— heavily WP-hook dependent, needs WP test suite stub improvements
Integration tests
SignatureVerificationIntegrationTest.php— needs real crypto operationsWpCliCompatTest.php— theme support is marked TODO in production code
HTTP tests
AdminAjaxHttpTest.php— needs stable admin-ajax endpoint responses from mock serverPluginsApiHttpTest.php— covered by browser DID search testsUpdateTransientShapeHttpTest.php— covered in integration update transient tests
Browser tests
install-activate-update.spec.ts(@slow) — needs stable plugin data in mock serveravatar-upload.spec.ts— needs file upload mock/page in Playwrightupdate-error-row.spec.ts— needs seeded update-error cache state
CI & tooling
- Coverage reporting in CI (via
composer run coverage:fullin Docker) - Slack/Discord notification on CI failure
- Regular infection runs in CI on main/RC branches
- Set
minMsithreshold in infection.json once baseline reaches target (e.g. 20%)
Test quality
- Raise overall MSI by expanding unit test coverage to admin/settings/upgrades modules
- Run infection against the full
inc/tree (remove exclusions) and kill escaped mutants
Parallelizable work
Phases 3–7 (unit tests) are largely independent per module and can be parallelized across contributors. Phases 8–11 are sequential — each builds on the previous. Phase 12 depends on all tests being in place. Phase 14 items are independent and can be tackled in any order.