Files
WP-Content-Sync/docs/superpowers/plans/2026-04-26-wordpress-content-sync-admin-hardening.md
T
2026-05-06 22:48:16 -05:00

28 KiB

Admin Workflow and Hardening Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Complete the plugin's usable admin workflow for configuring sync pairs, testing connections, importing/exporting package files, viewing operation results/logs, and running a final smoke checklist with hardened security and user-facing errors.

Architecture: Phase 6 keeps the current import-first sync engine and adds admin workflows around it. Settings stay option-backed, state-changing admin actions use dedicated controllers with capability and nonce checks, and templates receive pre-sanitized view data while still escaping every output at render time.

Tech Stack: PHP 7.4, WordPress admin APIs, WordPress HTTP/REST APIs, PHPUnit, PHPStan, PHPCS/WPCS, existing SettingsRepository, RestTransportClient, JsonFileTransport, SyncEngine, and OptionLogger.


File Structure

  • src/Settings/Settings.php: expand typed settings to include sync pair credentials, URL mappings, selected content types, default direction, and retention/debug controls.
  • src/Admin/AdminNotice.php: value object for redirect-driven admin notices with type, message, and optional result context.
  • src/Admin/AdminNoticeRepository.php: converts $_GET query flags into safe notices for templates.
  • src/Admin/SettingsController.php: handles settings saves through admin_post_wpcs_save_settings.
  • src/Admin/ConnectionTestController.php: handles connection diagnostics through admin_post_wpcs_test_connection.
  • src/Admin/FileExportController.php: exports a valid empty package scaffold for a configured pair until full extraction is implemented.
  • src/Admin/LogController.php: handles log clearing through admin_post_wpcs_clear_logs.
  • src/Admin/AdminPage.php: registers the new controllers and passes settings/notices/logs to the dashboard template.
  • src/Logging/OptionLogger.php: add safe read/clear helpers for operation history.
  • src/Transport/RestTransportClient.php: treat REST accepted: false JSON responses as remote rejections.
  • templates/admin/dashboard.php: replace the placeholder dashboard with forms for settings, diagnostics, file import/export, operation history, and smoke guidance.
  • tests/Unit/Admin/*Test.php: unit coverage for notices, settings save, connection diagnostics, export, log clearing, and dashboard rendering.
  • tests/Unit/SettingsTest.php: settings expansion and sanitization coverage.
  • tests/Unit/Transport/RestTransportClientTest.php: REST rejected-body coverage.
  • docs/smoke/phase-6-admin-hardening.md: manual smoke checklist and known local environment notes.

Task 1: Expand Settings for Admin Workflow

Files:

  • Modify: src/Settings/Settings.php

  • Modify: tests/Unit/SettingsTest.php

  • Step 1: Write failing settings tests

Add tests to tests/Unit/SettingsTest.php:

public function test_it_sanitizes_full_admin_workflow_settings(): void {
	$settings = Settings::fromArray(
		array(
			'sync_pairs' => array(
				array(
					'name'                 => '<b>Production to Staging</b>',
					'source_url'           => 'https://example.test/',
					'destination_url'      => 'https://staging.example.test/',
					'username'             => '<script>codex</script>',
					'application_password' => 'secret app password',
					'default_direction'    => 'push',
					'content_types'        => array( 'posts', 'terms', 'media', 'bad_type' ),
					'url_mappings'         => array(
						array(
							'source'      => 'https://example.test',
							'destination' => 'https://staging.example.test',
						),
					),
				),
			),
			'log_retention' => '50',
			'debug_logging' => '1',
		)
	);

	$pairs = $settings->syncPairs();

	self::assertSame( 'Production to Staging', $pairs[0]['name'] );
	self::assertSame( 'https://example.test/', $pairs[0]['source_url'] );
	self::assertSame( 'https://staging.example.test/', $pairs[0]['destination_url'] );
	self::assertSame( 'codex', $pairs[0]['username'] );
	self::assertSame( 'secret app password', $pairs[0]['application_password'] );
	self::assertSame( 'push', $pairs[0]['default_direction'] );
	self::assertSame( array( 'posts', 'terms', 'media' ), $pairs[0]['content_types'] );
	self::assertSame(
		array(
			array(
				'source'      => 'https://example.test',
				'destination' => 'https://staging.example.test',
			),
		),
		$pairs[0]['url_mappings']
	);
	self::assertSame( 50, $settings->logRetention() );
	self::assertTrue( $settings->debugLoggingEnabled() );
}
  • Step 2: Run tests to verify failure

Run: composer test -- --filter SettingsTest

Expected: FAIL because logRetention(), debugLoggingEnabled(), and expanded sync pair keys do not exist.

  • Step 3: Implement settings expansion

Update Settings to support:

private const DIRECTIONS     = array( 'push', 'pull' );
private const CONTENT_TYPES  = array( 'posts', 'terms', 'media', 'custom_post_types' );
private const MIN_LOGS       = 10;
private const MAX_LOGS       = 1000;

public function logRetention(): int;
public function debugLoggingEnabled(): bool;

Update sanitizeSyncPairs() so each pair returns:

array(
	'name'                 => $name,
	'source_url'           => $source_url,
	'destination_url'      => $destination_url,
	'username'             => $username,
	'application_password' => $application_password,
	'default_direction'    => $direction,
	'content_types'        => $content_types,
	'url_mappings'         => $url_mappings,
)

Rules:

  • Preserve legacy name, source_url, and destination_url behavior.

  • Sanitize username with sanitize_text_field().

  • Sanitize application_password with sanitize_text_field() and never log it.

  • Keep only allowed content types.

  • Keep only URL mappings where both source and destination are non-empty URLs.

  • Clamp log retention between 10 and 1000.

  • Default direction to push.

  • Step 4: Run focused tests

Run: composer test -- --filter SettingsTest

Expected: PASS.

  • Step 5: Commit
git add src/Settings/Settings.php tests/Unit/SettingsTest.php
git commit -m "feat: expand admin sync settings"

Task 2: Admin Notices

Files:

  • Create: src/Admin/AdminNotice.php

  • Create: src/Admin/AdminNoticeRepository.php

  • Create: tests/Unit/Admin/AdminNoticeRepositoryTest.php

  • Step 1: Write failing notice tests

Create tests/Unit/Admin/AdminNoticeRepositoryTest.php:

<?php
namespace WPContentSync\Tests\Unit\Admin;

use PHPUnit\Framework\TestCase;
use WPContentSync\Admin\AdminNoticeRepository;

class AdminNoticeRepositoryTest extends TestCase {
	protected function tearDown(): void {
		$_GET = array();
		parent::tearDown();
	}

	public function test_it_builds_import_success_notices(): void {
		$_GET['wpcs_imported'] = '1';
		$notices = ( new AdminNoticeRepository() )->current();

		self::assertSame( 'success', $notices[0]->type() );
		self::assertSame( 'The package JSON file was imported successfully.', $notices[0]->message() );
	}

	public function test_it_sanitizes_error_notices(): void {
		$_GET['wpcs_import_error'] = '<script>Bad package</script>';
		$notices = ( new AdminNoticeRepository() )->current();

		self::assertSame( 'error', $notices[0]->type() );
		self::assertSame( 'Bad package', $notices[0]->message() );
	}
}
  • Step 2: Run tests to verify failure

Run: composer test -- --filter AdminNoticeRepositoryTest

Expected: FAIL with missing classes.

  • Step 3: Implement notices

Create AdminNotice with:

public function __construct( string $type, string $message );
public function type(): string;
public function message(): string;

Allowed types: success, warning, error, info; fallback to info.

Create AdminNoticeRepository::current(): array returning notices for:

  • wpcs_imported=1: success, imported successfully.

  • wpcs_import_error: sanitized error message.

  • wpcs_settings_saved=1: success, settings saved.

  • wpcs_connection_ok=1: success, REST connection succeeded.

  • wpcs_connection_error: sanitized error message.

  • wpcs_logs_cleared=1: success, logs cleared.

  • wpcs_export_error: sanitized error message.

  • Step 4: Run focused tests

Run: composer test -- --filter AdminNoticeRepositoryTest

Expected: PASS.

  • Step 5: Commit
git add src/Admin/AdminNotice.php src/Admin/AdminNoticeRepository.php tests/Unit/Admin/AdminNoticeRepositoryTest.php
git commit -m "feat: add admin notices"

Task 3: Settings Save Controller

Files:

  • Create: src/Admin/SettingsController.php

  • Modify: src/Plugin.php

  • Create: tests/Unit/Admin/SettingsControllerTest.php

  • Modify: tests/Unit/PluginTest.php

  • Step 1: Write failing controller tests

Create tests/Unit/Admin/SettingsControllerTest.php:

<?php
namespace WPContentSync\Tests\Unit\Admin;

use PHPUnit\Framework\TestCase;
use WPContentSync\Admin\SettingsController;
use WPContentSync\Settings\SettingsRepository;

class SettingsControllerTest extends TestCase {
	protected function tearDown(): void {
		unset( $GLOBALS['wpcs_current_user_can'], $GLOBALS['wpcs_nonce_valid'], $GLOBALS['wpcs_redirect_location'], $GLOBALS['wpcs_test_options'] );
		$_POST = array();
		parent::tearDown();
	}

	public function test_it_saves_settings_with_nonce_and_capability(): void {
		$GLOBALS['wpcs_current_user_can']['manage_options'] = true;
		$GLOBALS['wpcs_nonce_valid']['wpcs_save_settings']['wpcs_settings_nonce'] = true;
		$_POST['wpcs_settings'] = array(
			'logging_level' => 'debug',
			'conflict_strategy' => 'manual_review',
		);

		( new SettingsController( new SettingsRepository() ) )->handleSave();

		self::assertSame( 'debug', $GLOBALS['wpcs_test_options'][ SettingsRepository::OPTION_NAME ]['logging_level'] );
		self::assertStringContainsString( 'wpcs_settings_saved=1', $GLOBALS['wpcs_redirect_location'] );
	}
}
  • Step 2: Run tests to verify failure

Run: composer test -- --filter SettingsControllerTest

Expected: FAIL with missing SettingsController.

  • Step 3: Implement controller

Create SettingsController:

final class SettingsController {
	private SettingsRepository $settings_repository;

	public function __construct( SettingsRepository $settings_repository ) {}
	public function register(): void;
	public function handleSave(): void;
}

Behavior:

  • register() hooks admin_post_wpcs_save_settings.
  • handleSave() requires manage_options.
  • Verify nonce wpcs_save_settings / wpcs_settings_nonce.
  • Read $_POST['wpcs_settings'], unslash with wp_unslash(), require array.
  • Persist through SettingsRepository::save( Settings::fromArray( $data ) ).
  • Redirect to dashboard with wpcs_settings_saved=1.

Update Plugin::create() to register SettingsController. Update Plugin::register() to call SettingsController::register(). Update PluginTest to assert the service exists and the hook is registered.

  • Step 4: Run focused tests

Run: composer test -- --filter "SettingsControllerTest|PluginTest"

Expected: PASS.

  • Step 5: Commit
git add src/Admin/SettingsController.php src/Plugin.php tests/Unit/Admin/SettingsControllerTest.php tests/Unit/PluginTest.php
git commit -m "feat: save admin sync settings"

Task 4: Dashboard Settings Form

Files:

  • Modify: templates/admin/dashboard.php

  • Modify: src/Admin/AdminPage.php

  • Modify: tests/Unit/Admin/DashboardTemplateTest.php

  • Step 1: Write failing dashboard tests

Add tests:

public function test_it_renders_settings_form_with_nonce_and_escaped_pair_values(): void {
	$settings = Settings::fromArray(
		array(
			'sync_pairs' => array(
				array(
					'name' => '<b>Staging</b>',
					'source_url' => 'https://example.test',
					'destination_url' => 'https://staging.example.test',
					'username' => 'codex',
				),
			),
		)
	);

	$output = $this->renderDashboard( $settings );

	self::assertStringContainsString( 'action="https://example.test/wp-admin/admin-post.php"', $output );
	self::assertStringContainsString( 'name="action" value="wpcs_save_settings"', $output );
	self::assertStringContainsString( 'Staging', $output );
	self::assertStringNotContainsString( '<b>Staging</b>', $output );
	self::assertStringContainsString( 'name="wpcs_settings[sync_pairs][0][application_password]"', $output );
}

Update renderDashboard() to accept optional Settings $settings.

  • Step 2: Run test to verify failure

Run: composer test -- --filter DashboardTemplateTest

Expected: FAIL because the dashboard does not render the settings form.

  • Step 3: Implement dashboard form

Update dashboard.php:

  • Replace the foundation notice with a "Configuration" section.

  • Render a form posting to admin-post.php.

  • Include action=wpcs_save_settings.

  • Include nonce wp_nonce_field( 'wpcs_save_settings', 'wpcs_settings_nonce' ).

  • Render existing sync pairs; if none exist, render one blank pair row.

  • Fields per pair: name, source URL, destination URL, username, application password, default direction, content types, URL mappings.

  • Never render saved application password values back into the password field; render placeholder text only.

  • Escape every attribute with esc_attr() and every text node with esc_html().

  • Step 4: Run focused tests

Run: composer test -- --filter DashboardTemplateTest

Expected: PASS.

  • Step 5: Commit
git add templates/admin/dashboard.php src/Admin/AdminPage.php tests/Unit/Admin/DashboardTemplateTest.php
git commit -m "feat: render admin settings workflow"

Task 5: Connection Diagnostics

Files:

  • Create: src/Admin/ConnectionTestController.php

  • Modify: src/Plugin.php

  • Create: tests/Unit/Admin/ConnectionTestControllerTest.php

  • Modify: tests/Unit/Admin/DashboardTemplateTest.php

  • Modify: templates/admin/dashboard.php

  • Step 1: Write failing diagnostics tests

Create ConnectionTestControllerTest that injects RestTransportClient, posts pair_index=0, and verifies:

  • capability and nonce are required.
  • success redirects with wpcs_connection_ok=1.
  • failures redirect with wpcs_connection_error.
  • application password is never placed in redirect query args.

Use settings fixture:

update_option(
	SettingsRepository::OPTION_NAME,
	array(
		'sync_pairs' => array(
			array(
				'name' => 'Staging',
				'destination_url' => 'https://destination.test',
				'username' => 'codex',
				'application_password' => 'app-pass',
			),
		),
	),
	false
);
  • Step 2: Run tests to verify failure

Run: composer test -- --filter ConnectionTestControllerTest

Expected: FAIL with missing controller.

  • Step 3: Implement diagnostics controller

Create ConnectionTestController:

  • Hook admin_post_wpcs_test_connection.
  • Require manage_options.
  • Verify nonce wpcs_test_connection / wpcs_connection_nonce.
  • Read pair_index as integer.
  • Load selected pair from settings.
  • Call RestTransportClient::testConnection( destination_url, username, application_password ).
  • On success redirect with wpcs_connection_ok=1.
  • On RestTransportException redirect with sanitized wpcs_connection_error.
  • Log success/failure without credentials.

Update Plugin to register the controller. Update dashboard to add a "Test REST Connection" button per pair with nonce.

  • Step 4: Run focused tests

Run: composer test -- --filter "ConnectionTestControllerTest|DashboardTemplateTest|PluginTest"

Expected: PASS.

  • Step 5: Commit
git add src/Admin/ConnectionTestController.php src/Plugin.php templates/admin/dashboard.php tests/Unit/Admin/ConnectionTestControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php tests/Unit/PluginTest.php
git commit -m "feat: add connection diagnostics"

Task 6: REST Transport Rejected Body Handling

Files:

  • Modify: src/Transport/RestTransportClient.php

  • Modify: tests/Unit/Transport/RestTransportClientTest.php

  • Step 1: Write failing REST client test

Add:

public function test_it_throws_when_receive_endpoint_returns_accepted_false(): void {
	$GLOBALS['wpcs_http_response'] = array(
		'response' => array( 'code' => 200 ),
		'body'     => '{"accepted":false,"import":{"errors":["Posts failed."]}}',
	);
	$client = new RestTransportClient();

	$this->expectException( RestTransportException::class );
	$this->expectExceptionMessage( 'Posts failed.' );

	$client->sendPackage( 'https://destination.test', 'codex', 'app-pass', $this->package() );
}
  • Step 2: Run test to verify failure

Run: composer test -- --filter RestTransportClientTest

Expected: FAIL because HTTP 200 currently counts as success.

  • Step 3: Implement body inspection

Update assertSuccessfulResponse():

  • If status code is expected, decode body.

  • If decoded body has accepted === false, throw RestTransportException::remoteRejected().

  • Prefer first string in import.errors, then first string in errors, then message, then fallback.

  • Step 4: Run focused tests

Run: composer test -- --filter RestTransportClientTest

Expected: PASS.

  • Step 5: Commit
git add src/Transport/RestTransportClient.php tests/Unit/Transport/RestTransportClientTest.php
git commit -m "fix: reject failed rest imports"

Task 7: Operation Logs and Clear Action

Files:

  • Modify: src/Logging/OptionLogger.php

  • Create: src/Admin/LogController.php

  • Modify: src/Plugin.php

  • Modify: templates/admin/dashboard.php

  • Modify: tests/Unit/OptionLoggerTest.php

  • Create: tests/Unit/Admin/LogControllerTest.php

  • Modify: tests/Unit/Admin/DashboardTemplateTest.php

  • Step 1: Write failing logger/controller tests

Add OptionLoggerTest coverage for:

$logger = new OptionLogger( 10 );
$logger->info( 'Imported content package.' );
self::assertCount( 1, $logger->entries() );
$logger->clear();
self::assertSame( array(), $logger->entries() );

Create LogControllerTest verifying:

  • capability and nonce are required.

  • handleClear() clears OptionLogger::OPTION_NAME.

  • redirect contains wpcs_logs_cleared=1.

  • Step 2: Run tests to verify failure

Run: composer test -- --filter "OptionLoggerTest|LogControllerTest"

Expected: FAIL with missing methods/controller.

  • Step 3: Implement logs

Update OptionLogger:

public function entries(): array;
public function clear(): void;

Create LogController:

  • Hook admin_post_wpcs_clear_logs.
  • Require manage_options.
  • Verify nonce wpcs_clear_logs / wpcs_logs_nonce.
  • Clear logs.
  • Redirect with wpcs_logs_cleared=1.

Update dashboard:

  • Render a recent operation history table.

  • Show timestamp, level, message, and redacted context summary.

  • Add "Clear Logs" form with nonce.

  • Step 4: Run focused tests

Run: composer test -- --filter "OptionLoggerTest|LogControllerTest|DashboardTemplateTest|PluginTest"

Expected: PASS.

  • Step 5: Commit
git add src/Logging/OptionLogger.php src/Admin/LogController.php src/Plugin.php templates/admin/dashboard.php tests/Unit/OptionLoggerTest.php tests/Unit/Admin/LogControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php tests/Unit/PluginTest.php
git commit -m "feat: add operation log controls"

Task 8: File Export Scaffold

Files:

  • Create: src/Admin/FileExportController.php

  • Modify: src/Plugin.php

  • Modify: templates/admin/dashboard.php

  • Create: tests/Unit/Admin/FileExportControllerTest.php

  • Modify: tests/Unit/Admin/DashboardTemplateTest.php

  • Step 1: Write failing export tests

Create FileExportControllerTest verifying:

  • capability and nonce are required.

  • a configured pair exports a valid JSON package with all four record buckets.

  • response headers include Content-Type: application/json.

  • no content mutation occurs during export.

  • Step 2: Run tests to verify failure

Run: composer test -- --filter FileExportControllerTest

Expected: FAIL with missing controller.

  • Step 3: Implement export scaffold

Create FileExportController:

  • Hook admin_post_wpcs_export_package.
  • Require manage_options.
  • Verify nonce wpcs_export_package / wpcs_export_nonce.
  • Read pair_index.
  • Build an empty ContentPackage using the selected pair:
    • source.site_url from pair source URL.
    • destination.site_url from pair destination URL.
    • manifest counts all zero.
    • records buckets all empty.
    • checksum from PackageChecksum::records().
  • Export with JsonFileTransport::export().
  • Send JSON download headers.

Update dashboard with an "Export Empty Package" form and helper copy stating full extraction will be added in a later slice.

  • Step 4: Run focused tests

Run: composer test -- --filter "FileExportControllerTest|DashboardTemplateTest|PluginTest"

Expected: PASS.

  • Step 5: Commit
git add src/Admin/FileExportController.php src/Plugin.php templates/admin/dashboard.php tests/Unit/Admin/FileExportControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php tests/Unit/PluginTest.php
git commit -m "feat: add package export scaffold"

Task 9: Import Result UI Hardening

Files:

  • Modify: src/Admin/FileImportController.php

  • Modify: templates/admin/dashboard.php

  • Modify: tests/Unit/Admin/FileImportControllerTest.php

  • Modify: tests/Unit/Admin/DashboardTemplateTest.php

  • Step 1: Write failing result tests

Add file import test asserting success redirects include counts:

self::assertStringContainsString( 'wpcs_created=0', $GLOBALS['wpcs_redirect_location'] );
self::assertStringContainsString( 'wpcs_updated=0', $GLOBALS['wpcs_redirect_location'] );
self::assertStringContainsString( 'wpcs_skipped=0', $GLOBALS['wpcs_redirect_location'] );
self::assertStringContainsString( 'wpcs_conflicts=0', $GLOBALS['wpcs_redirect_location'] );

Add dashboard test asserting the success notice includes created/updated/skipped/conflict counts from sanitized query args.

  • Step 2: Run tests to verify failure

Run: composer test -- --filter "FileImportControllerTest|DashboardTemplateTest"

Expected: FAIL because result counts are not in redirects or UI.

  • Step 3: Implement result count redirects

Update FileImportController success redirect:

array(
	'wpcs_imported'  => '1',
	'wpcs_created'   => (string) $result->created(),
	'wpcs_updated'   => (string) $result->updated(),
	'wpcs_skipped'   => (string) $result->skipped(),
	'wpcs_conflicts' => (string) $result->conflicts(),
)

Update dashboard to display counts with absint() and escaped labels.

  • Step 4: Run focused tests

Run: composer test -- --filter "FileImportControllerTest|DashboardTemplateTest"

Expected: PASS.

  • Step 5: Commit
git add src/Admin/FileImportController.php templates/admin/dashboard.php tests/Unit/Admin/FileImportControllerTest.php tests/Unit/Admin/DashboardTemplateTest.php
git commit -m "feat: show import result summaries"

Task 10: Smoke Checklist Documentation

Files:

  • Create: docs/smoke/phase-6-admin-hardening.md

  • Step 1: Create smoke checklist

Create docs/smoke/phase-6-admin-hardening.md with:

# Phase 6 Admin Hardening Smoke Checklist

## Environment

- WordPress site URL:
- Plugin branch/commit:
- PHP version:
- WordPress version:

## Checks

- [ ] Plugin activates without fatal errors.
- [ ] Tools -> Content Sync loads for an administrator.
- [ ] Non-administrators cannot access the dashboard.
- [ ] Settings save rejects missing/invalid nonce.
- [ ] Settings save persists sync pair name, URLs, username, content types, direction, and URL mappings.
- [ ] Saved application password is not rendered back into the password field.
- [ ] REST status endpoint rejects unauthenticated HTTP requests.
- [ ] REST status endpoint accepts authenticated requests when the server passes `Authorization` to PHP.
- [ ] Connection test succeeds with a valid application password.
- [ ] Connection test shows an actionable error for invalid credentials.
- [ ] Invalid package file import redirects with an error notice.
- [ ] Valid empty package import redirects with success and result counts.
- [ ] REST package POST accepts a valid package and includes import counts.
- [ ] REST package POST rejects invalid package data.
- [ ] Operation log table shows recent events with redacted credential-like fields.
- [ ] Clear logs requires nonce and removes log entries.
- [ ] Export scaffold downloads valid JSON.

## Local Notes

- On Herd/nginx, direct HTTP Basic auth may require forwarding the `Authorization` header. If HTTP application-password smoke returns 401 while internal REST dispatch passes, verify server auth header configuration before treating it as a plugin failure.
  • Step 2: Commit docs
git add docs/smoke/phase-6-admin-hardening.md
git commit -m "docs: add admin hardening smoke checklist"

Task 11: Full Phase 6 Verification

Files:

  • Verify all files changed in Tasks 1-10.

  • Step 1: Run Composer validation

Run: composer validate --strict

Expected: PASS with ./composer.json is valid.

  • Step 2: Run PHPCS

Run: composer lint

Expected: PASS with no PHPCS errors or warnings.

  • Step 3: Run PHPStan

Run: vendor\bin\phpstan analyse --memory-limit=1G

Expected: PASS with [OK] No errors.

  • Step 4: Run PHPUnit

Run: composer test

Expected: PASS with all unit tests passing.

  • Step 5: Copy runtime files to Herd test plugin

Run:

Copy-Item -Path src,templates,wp-content-sync.php,composer.json -Destination 'C:\Users\ksolo\Herd\basic-wp\wp-content\plugins\WP-Content-Sync' -Recurse -Force

Expected: command exits 0.

  • Step 6: Run manual smoke checklist

Use docs/smoke/phase-6-admin-hardening.md.

Required checks before completion:

  • Plugin active.

  • Admin dashboard loads after login.

  • Settings save works and rejects invalid nonce.

  • Connection diagnostics show success/failure notices.

  • Invalid file import redirects with error.

  • Valid empty file import redirects with success counts.

  • REST unauthenticated status returns 401.

  • REST valid and invalid package behavior is verified by HTTP application password if server passes Basic auth, otherwise by internal REST dispatch plus documented server caveat.

  • Logs render and clear.

  • Export scaffold downloads valid JSON.

  • Step 7: Commit verification notes if changed

If smoke notes are updated with environment results:

git add docs/smoke/phase-6-admin-hardening.md
git commit -m "docs: record phase 6 smoke results"

Spec Coverage

  • Sync pair configuration is covered by Tasks 1, 3, and 4.
  • Credentials and authentication setup are covered by Tasks 1, 4, and 5, with password fields not rendered back to the browser.
  • URL mapping configuration is covered by Task 1 and Task 4.
  • Content type selection and default sync direction are covered by Task 1 and Task 4.
  • Connection diagnostics are covered by Task 5.
  • Import screens and user-facing import errors are covered by Tasks 2, 4, and 9.
  • Export screen is covered by Task 8 as a valid package scaffold until full extraction is implemented.
  • Operation history and debug/log controls are covered by Task 7.
  • REST failure hardening is covered by Task 6.
  • Nonces/capabilities for state-changing admin actions are covered by Tasks 3, 5, 7, 8, and existing file import tests.
  • Final smoke/integration checklist is covered by Tasks 10 and 11.

Deferred Work

  • Full content extraction for non-empty package exports remains a later slice because Phase 5 built import orchestration and handlers, not source extraction orchestration.
  • Background queues, progress polling, and cancelable long-running operations remain a later scalability slice.
  • HTTP Basic authentication pass-through is a server configuration concern; plugin REST behavior remains covered by controller tests and internal REST dispatch smoke.

Placeholder Scan

  • No unresolved placeholder markers are intentionally included.
  • Each task names exact files, expected failing tests, implementation behavior, verification commands, and commit messages.
  • Deferred work is explicitly scoped with rationale and is not required for Phase 6 exit criteria in this implementation slice.