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 withtype,message, and optional result context.src/Admin/AdminNoticeRepository.php: converts$_GETquery flags into safe notices for templates.src/Admin/SettingsController.php: handles settings saves throughadmin_post_wpcs_save_settings.src/Admin/ConnectionTestController.php: handles connection diagnostics throughadmin_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 throughadmin_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 RESTaccepted: falseJSON 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, anddestination_urlbehavior. -
Sanitize
usernamewithsanitize_text_field(). -
Sanitize
application_passwordwithsanitize_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
10and1000. -
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()hooksadmin_post_wpcs_save_settings.handleSave()requiresmanage_options.- Verify nonce
wpcs_save_settings/wpcs_settings_nonce. - Read
$_POST['wpcs_settings'], unslash withwp_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 withesc_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_indexas integer. - Load selected pair from settings.
- Call
RestTransportClient::testConnection( destination_url, username, application_password ). - On success redirect with
wpcs_connection_ok=1. - On
RestTransportExceptionredirect with sanitizedwpcs_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, throwRestTransportException::remoteRejected(). -
Prefer first string in
import.errors, then first string inerrors, thenmessage, 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()clearsOptionLogger::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
ContentPackageusing the selected pair:source.site_urlfrom pair source URL.destination.site_urlfrom 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.