rem last build change
This commit is contained in:
@@ -55,10 +55,6 @@
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-behavior: auto;
|
||||
transform: translateZ(0);
|
||||
will-change: scroll-position;
|
||||
}
|
||||
|
||||
.app-shell.builder-single>* {
|
||||
|
||||
@@ -1520,7 +1520,7 @@ function closeHistoryModal() {
|
||||
if (el.historyModal) el.historyModal.style.display = 'none';
|
||||
}
|
||||
|
||||
function scrollChatToBottom() {
|
||||
function scrollChatToBottom(force = false) {
|
||||
if (!el.chatArea) return;
|
||||
const target = el.chatArea;
|
||||
|
||||
@@ -1529,48 +1529,91 @@ function scrollChatToBottom() {
|
||||
target.scrollTop = target.scrollHeight;
|
||||
};
|
||||
|
||||
// Use multiple scroll techniques in sequence for reliability
|
||||
// 1. Immediate scroll
|
||||
// Use multiple techniques to ensure scroll happens after content is rendered
|
||||
// 1. Immediate scroll attempt
|
||||
doScroll();
|
||||
|
||||
// 2. After DOM updates
|
||||
setTimeout(doScroll, 50);
|
||||
setTimeout(doScroll, 100);
|
||||
setTimeout(doScroll, 200);
|
||||
setTimeout(doScroll, 500);
|
||||
// 2. After short delays to allow DOM updates
|
||||
setTimeout(() => { doScroll(); }, 10);
|
||||
setTimeout(() => { doScroll(); }, 50);
|
||||
setTimeout(() => { doScroll(); }, 100);
|
||||
setTimeout(() => { doScroll(); }, 250);
|
||||
setTimeout(() => { doScroll(); }, 500);
|
||||
|
||||
// 3. After content fully renders
|
||||
setTimeout(doScroll, 1000);
|
||||
// 3. Longer delay for content to fully settle (especially on page load)
|
||||
setTimeout(() => { doScroll(); }, 1000);
|
||||
setTimeout(() => { doScroll(); }, 2000);
|
||||
|
||||
// 4. requestAnimationFrame for smooth scrolling
|
||||
// 4. Use requestAnimationFrame for smooth scrolling
|
||||
requestAnimationFrame(() => {
|
||||
doScroll();
|
||||
requestAnimationFrame(doScroll);
|
||||
requestAnimationFrame(() => {
|
||||
doScroll();
|
||||
requestAnimationFrame(() => {
|
||||
doScroll();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// 5. Scroll to last message element if it exists (most reliable method)
|
||||
setTimeout(() => {
|
||||
const lastMessage = target.querySelector('.message:last-child');
|
||||
if (lastMessage) {
|
||||
lastMessage.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
||||
} else {
|
||||
// Fallback to regular scroll
|
||||
doScroll();
|
||||
// 5. Wait for ALL images to load before scrolling
|
||||
const images = target.querySelectorAll('img');
|
||||
let imagesToLoad = 0;
|
||||
|
||||
images.forEach((img) => {
|
||||
if (!img.complete) {
|
||||
imagesToLoad++;
|
||||
img.addEventListener('load', () => {
|
||||
imagesToLoad--;
|
||||
doScroll();
|
||||
// Scroll again after a short delay to ensure layout is updated
|
||||
setTimeout(() => doScroll(), 100);
|
||||
}, { once: true });
|
||||
|
||||
img.addEventListener('error', () => {
|
||||
imagesToLoad--;
|
||||
doScroll();
|
||||
}, { once: true });
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// 6. ResizeObserver for dynamic content
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const resizeObserver = new ResizeObserver(() => doScroll());
|
||||
resizeObserver.observe(target);
|
||||
setTimeout(() => resizeObserver.disconnect(), 2000);
|
||||
});
|
||||
|
||||
// If there are images loading, scroll again after they all complete
|
||||
if (imagesToLoad > 0) {
|
||||
const checkImagesLoaded = setInterval(() => {
|
||||
if (imagesToLoad === 0) {
|
||||
clearInterval(checkImagesLoaded);
|
||||
doScroll();
|
||||
setTimeout(() => doScroll(), 100);
|
||||
setTimeout(() => doScroll(), 500);
|
||||
}
|
||||
}, 100);
|
||||
|
||||
// Stop checking after 5 seconds to avoid infinite interval
|
||||
setTimeout(() => clearInterval(checkImagesLoaded), 5000);
|
||||
}
|
||||
|
||||
// 7. MutationObserver for DOM changes
|
||||
|
||||
// 6. Use ResizeObserver to detect when content height changes
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
const resizeObserver = new ResizeObserver((entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.target === target) {
|
||||
doScroll();
|
||||
}
|
||||
}
|
||||
});
|
||||
resizeObserver.observe(target);
|
||||
|
||||
// Stop observing after a while
|
||||
setTimeout(() => resizeObserver.disconnect(), 3000);
|
||||
}
|
||||
|
||||
// 7. Also scroll when any content finishes rendering (using MutationObserver)
|
||||
if (typeof MutationObserver !== 'undefined') {
|
||||
const observer = new MutationObserver(() => doScroll());
|
||||
const observer = new MutationObserver(() => {
|
||||
doScroll();
|
||||
});
|
||||
observer.observe(target, { childList: true, subtree: true });
|
||||
setTimeout(() => observer.disconnect(), 2000);
|
||||
// Stop observing after a while to avoid memory leaks
|
||||
setTimeout(() => observer.disconnect(), 3000);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1984,22 +2027,7 @@ function renderMessages(session) {
|
||||
}
|
||||
}
|
||||
|
||||
// Scroll to bottom after DOM updates
|
||||
scrollChatToBottom();
|
||||
|
||||
// Additional scroll with longer delay to ensure all content is rendered
|
||||
setTimeout(() => {
|
||||
scrollChatToBottom();
|
||||
// Force scroll by accessing scrollHeight directly
|
||||
if (el.chatArea) {
|
||||
el.chatArea.scrollTop = el.chatArea.scrollHeight;
|
||||
// Scroll last element into view as fallback
|
||||
const lastMessage = el.chatArea.querySelector('.message:last-child');
|
||||
if (lastMessage) {
|
||||
lastMessage.scrollIntoView({ block: 'end' });
|
||||
}
|
||||
}
|
||||
}, 200);
|
||||
|
||||
updateExportButtonVisibility(session);
|
||||
}
|
||||
|
||||
963
external-wp-testing-plan.md
Normal file
963
external-wp-testing-plan.md
Normal file
@@ -0,0 +1,963 @@
|
||||
# PluginCompass External WordPress Testing System
|
||||
|
||||
## Executive Summary
|
||||
|
||||
A production-ready testing system that uses an externally hosted WordPress site with WP-CLI to verify plugin functionality. Supports high concurrency (20+ sessions) through multisite subsite isolation, with two testing modes (automated WP-CLI commands and visual browser testing).
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ PluginCompass CLI Users │
|
||||
│ (20+ Concurrent Sessions) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ Test Orchestrator Layer │
|
||||
│ - Session Queue Manager (prevents conflicts) │
|
||||
│ - Subsite Provisioning (auto-creates test sites) │
|
||||
│ - Resource Cleanup (automatic teardown) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ External WordPress Multisite │
|
||||
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
|
||||
│ │ Session 1 │ │ Session 2 │ │ Session N │ │
|
||||
│ │ Subsite │ │ Subsite │ │ Subsite │ │
|
||||
│ │ /test-abc │ │ /test-def │ │ /test-xyz │ │
|
||||
│ └──────────────┘ └──────────────┘ └──────────────┘ │
|
||||
│ │
|
||||
│ Shared: Core WP, Plugins, Themes (isolated per subsite) │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Core Components
|
||||
|
||||
### 1. Test Orchestrator (`test_orchestrator.js`)
|
||||
|
||||
Central controller managing all test sessions.
|
||||
|
||||
**Responsibilities:**
|
||||
- Queue incoming test requests
|
||||
- Provision subsites automatically
|
||||
- Manage plugin installations
|
||||
- Coordinate cleanup
|
||||
- Prevent cross-session contamination
|
||||
|
||||
**Configuration:**
|
||||
```javascript
|
||||
const TEST_CONFIG = {
|
||||
// External WP Site Connection
|
||||
wpHost: process.env.TEST_WP_HOST || 'testsite.example.com',
|
||||
wpSshUser: process.env.TEST_WP_SSH_USER || 'wordpress',
|
||||
wpSshKey: process.env.TEST_WP_SSH_KEY || '~/.ssh/wp-test-key',
|
||||
wpPath: process.env.TEST_WP_PATH || '/var/www/html',
|
||||
|
||||
// Multisite Settings
|
||||
enableMultisite: true,
|
||||
subsitePrefix: 'test',
|
||||
subsiteDomain: 'testsite.example.com',
|
||||
|
||||
// Concurrency
|
||||
maxConcurrentTests: 20,
|
||||
queueTimeout: 300000, // 5 minutes
|
||||
testTimeout: 600000, // 10 minutes
|
||||
|
||||
// Cleanup
|
||||
autoCleanup: true,
|
||||
cleanupDelay: 3600000, // 1 hour after test completion
|
||||
|
||||
// Testing Modes
|
||||
enableVisualTesting: process.env.ENABLE_VISUAL_TESTING === 'true',
|
||||
visualTestBrowser: 'chromium', // chromium, firefox, webkit
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. New Tool: `test_plugin_external_wp`
|
||||
|
||||
**Tool Name:** `test_plugin_external_wp`
|
||||
|
||||
**Description:** Deploys and tests plugin on external WordPress site with full isolation. Automatically provisions subsite, installs dependencies, runs verification tests, and provides detailed results.
|
||||
|
||||
**Parameters:**
|
||||
```json
|
||||
{
|
||||
"name": "test_plugin_external_wp",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plugin_path": {
|
||||
"type": "string",
|
||||
"description": "Local path to generated plugin directory"
|
||||
},
|
||||
"test_mode": {
|
||||
"type": "string",
|
||||
"enum": ["cli", "visual", "both"],
|
||||
"default": "cli",
|
||||
"description": "Testing mode: cli (WP-CLI commands), visual (browser automation), or both"
|
||||
},
|
||||
"required_plugins": {
|
||||
"type": "array",
|
||||
"description": "Other plugins that must be installed for this plugin to work",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"plugin_slug": { "type": "string" },
|
||||
"version": { "type": "string" },
|
||||
"source": {
|
||||
"type": "string",
|
||||
"enum": ["wordpress.org", "url", "local"],
|
||||
"default": "wordpress.org"
|
||||
},
|
||||
"source_url": { "type": "string" },
|
||||
"activate": { "type": "boolean", "default": true }
|
||||
},
|
||||
"required": ["plugin_slug"]
|
||||
}
|
||||
},
|
||||
"test_scenarios": {
|
||||
"type": "array",
|
||||
"description": "Specific test scenarios to verify",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"type": {
|
||||
"type": "string",
|
||||
"enum": ["endpoint", "admin_page", "ajax", "shortcode", "hook", "visual", "custom"]
|
||||
},
|
||||
"url": { "type": "string" },
|
||||
"selector": { "type": "string" },
|
||||
"expected_text": { "type": "string" },
|
||||
"wp_cli_command": { "type": "string" },
|
||||
"assertions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status_code": { "type": "number" },
|
||||
"contains": { "type": "array", "items": { "type": "string" } },
|
||||
"not_contains": { "type": "array", "items": { "type": "string" } },
|
||||
"wp_cli_success": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["name", "type"]
|
||||
}
|
||||
},
|
||||
"visual_tests": {
|
||||
"type": "array",
|
||||
"description": "Visual testing scenarios (only used when test_mode is 'visual' or 'both')",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"url": { "type": "string" },
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["goto", "click", "fill", "wait", "screenshot", "evaluate"]
|
||||
},
|
||||
"selector": { "type": "string" },
|
||||
"value": { "type": "string" },
|
||||
"timeout": { "type": "number" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"assertions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"visible": { "type": "array", "items": { "type": "string" } },
|
||||
"hidden": { "type": "array", "items": { "type": "string" } },
|
||||
"text_contains": { "type": "array", "items": { "type": "string" } },
|
||||
"no_console_errors": { "type": "boolean" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"timeout": {
|
||||
"type": "number",
|
||||
"default": 300,
|
||||
"description": "Test timeout in seconds"
|
||||
},
|
||||
"auto_fix": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Automatically retry and fix on test failure"
|
||||
},
|
||||
"max_fix_attempts": {
|
||||
"type": "number",
|
||||
"default": 3,
|
||||
"description": "Maximum auto-fix attempts before giving up"
|
||||
}
|
||||
},
|
||||
"required": ["plugin_path"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Return Value:**
|
||||
```javascript
|
||||
{
|
||||
"ok": true,
|
||||
"session_id": "sess_abc123",
|
||||
"subsite_url": "https://testsite.example.com/test-abc123",
|
||||
"test_results": {
|
||||
"mode": "both",
|
||||
"cli_tests": {
|
||||
"passed": 5,
|
||||
"failed": 0,
|
||||
"results": [
|
||||
{
|
||||
"name": "Plugin activates without errors",
|
||||
"status": "passed",
|
||||
"command": "wp plugin activate test-plugin",
|
||||
"output": "Success: Activated 1 of 1 plugins.",
|
||||
"duration": 2340
|
||||
},
|
||||
{
|
||||
"name": "Custom login endpoint responds",
|
||||
"status": "passed",
|
||||
"command": "wp eval 'echo wp_remote_retrieve_body(wp_remote_get(home_url(\"/custom-login\")));'",
|
||||
"assertions": {
|
||||
"contains": ["<form", "login"],
|
||||
"status_code": 200
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"visual_tests": {
|
||||
"passed": 3,
|
||||
"failed": 0,
|
||||
"screenshots": [
|
||||
{
|
||||
"name": "login-page",
|
||||
"path": "/results/sess_abc123/login-page.png",
|
||||
"url": "https://testsite.example.com/test-abc123/custom-login"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"installed_plugins": [
|
||||
{ "slug": "woocommerce", "version": "8.0.0", "status": "active" },
|
||||
{ "slug": "test-plugin", "version": "1.0.0", "status": "active" }
|
||||
],
|
||||
"errors": [],
|
||||
"warnings": [],
|
||||
"duration": 45230,
|
||||
"cleanup_scheduled": "2024-01-15T10:30:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Modes
|
||||
|
||||
### Mode 1: WP-CLI Automated Testing
|
||||
|
||||
**Use Case:** Fast, deterministic testing without browser overhead
|
||||
|
||||
**How it works:**
|
||||
1. Deploy plugin to subsite
|
||||
2. Run WP-CLI commands to verify functionality
|
||||
3. Check HTTP endpoints via `wp eval` with `wp_remote_get()`
|
||||
4. Verify database state changes
|
||||
5. Check error logs
|
||||
|
||||
**Example Test Scenarios:**
|
||||
```javascript
|
||||
[
|
||||
{
|
||||
"name": "Plugin activates cleanly",
|
||||
"type": "custom",
|
||||
"wp_cli_command": "wp plugin activate {plugin_slug} --skip-plugins",
|
||||
"assertions": {
|
||||
"wp_cli_success": true,
|
||||
"not_contains": ["Fatal error", "Parse error", "Warning"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Custom endpoint accessible",
|
||||
"type": "endpoint",
|
||||
"url": "/custom-login",
|
||||
"wp_cli_command": "wp eval 'echo wp_remote_retrieve_response_code(wp_remote_get(home_url(\"/custom-login\")));'",
|
||||
"assertions": {
|
||||
"status_code": 200,
|
||||
"contains": ["<form", "name=\"log\""]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Rewrite rules flushed",
|
||||
"type": "custom",
|
||||
"wp_cli_command": "wp rewrite list --match=/custom-login --format=json",
|
||||
"assertions": {
|
||||
"wp_cli_success": true,
|
||||
"contains": ["custom-login"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "No PHP errors in logs",
|
||||
"type": "custom",
|
||||
"wp_cli_command": "tail -n 50 /var/log/wp-errors.log | grep -i 'test-plugin' || echo 'No errors found'",
|
||||
"assertions": {
|
||||
"not_contains": ["Fatal error", "Parse error", "test-plugin"]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Fast (5-30 seconds per test)
|
||||
- No browser dependencies
|
||||
- Perfect for API/endpoint testing
|
||||
- Can check database state directly
|
||||
- Scales to 100+ concurrent tests
|
||||
|
||||
---
|
||||
|
||||
### Mode 2: Visual Browser Testing
|
||||
|
||||
**Use Case:** UI verification, JavaScript functionality, styling checks
|
||||
|
||||
**How it works:**
|
||||
1. Deploy plugin to subsite
|
||||
2. Launch headless browser (Playwright)
|
||||
3. Navigate to specified URLs
|
||||
4. Perform user interactions (click, type, submit)
|
||||
5. Capture screenshots
|
||||
6. Verify visual elements and JavaScript execution
|
||||
|
||||
**Example Visual Tests:**
|
||||
```javascript
|
||||
[
|
||||
{
|
||||
"name": "Login form renders correctly",
|
||||
"url": "/custom-login",
|
||||
"actions": [
|
||||
{ "action": "goto", "url": "/custom-login" },
|
||||
{ "action": "screenshot", "name": "initial-load" },
|
||||
{ "action": "wait", "selector": "form" }
|
||||
],
|
||||
"assertions": {
|
||||
"visible": ["input[name='log']", "input[name='pwd']", ".submit-button"],
|
||||
"no_console_errors": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Add to cart button works",
|
||||
"url": "/shop",
|
||||
"actions": [
|
||||
{ "action": "goto", "url": "/shop" },
|
||||
{ "action": "click", "selector": "[data-product_id='123']" },
|
||||
{ "action": "wait", "timeout": 2000 },
|
||||
{ "action": "screenshot", "name": "after-click" }
|
||||
],
|
||||
"assertions": {
|
||||
"visible": [".cart-count"],
|
||||
"text_contains": ["1 item"],
|
||||
"no_console_errors": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Admin settings page loads",
|
||||
"url": "/wp-admin/admin.php?page=my-plugin",
|
||||
"actions": [
|
||||
{
|
||||
"action": "evaluate",
|
||||
"value": "document.querySelector('#user_login').value = 'admin'; document.querySelector('#user_pass').value = 'password'; document.querySelector('#wp-submit').click();"
|
||||
},
|
||||
{ "action": "wait", "url": "/wp-admin/admin.php?page=my-plugin" },
|
||||
{ "action": "screenshot", "name": "admin-page" }
|
||||
],
|
||||
"assertions": {
|
||||
"visible": [".wrap h1", "form"],
|
||||
"no_console_errors": true
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
**Benefits:**
|
||||
- Catches visual/styling issues
|
||||
- Verifies JavaScript execution
|
||||
- Tests actual user workflows
|
||||
- Screenshots provide proof
|
||||
- Finds timing/race condition issues
|
||||
|
||||
---
|
||||
|
||||
## Session Isolation Strategy
|
||||
|
||||
### High Concurrency Solution: Multisite Subsites
|
||||
|
||||
**Why Multisite:**
|
||||
- Each session gets completely isolated WordPress instance
|
||||
- Shared core (efficient) but isolated content/options
|
||||
- Can run 20+ tests simultaneously without conflicts
|
||||
- Automatic cleanup by deleting subsite
|
||||
|
||||
**Setup Requirements:**
|
||||
1. Convert hosted WP to multisite (one-time)
|
||||
2. Enable subdomain or subdirectory mode
|
||||
3. Configure wildcard DNS or subdirectory rewrite rules
|
||||
|
||||
**Subsite Provisioning:**
|
||||
```javascript
|
||||
class SubsiteProvisioner {
|
||||
async createTestSubsite(sessionId) {
|
||||
const slug = `test-${sessionId.substring(0, 8)}`;
|
||||
const title = `Test Environment ${sessionId}`;
|
||||
const adminEmail = 'test@example.com';
|
||||
|
||||
// Create subsite via WP-CLI
|
||||
await this.runWpCli(`
|
||||
wp site create
|
||||
--slug=${slug}
|
||||
--title="${title}"
|
||||
--email=${adminEmail}
|
||||
`);
|
||||
|
||||
// Configure subsite
|
||||
await this.runWpCli(`
|
||||
wp site switch --url=https://${this.config.subsiteDomain}/${slug}
|
||||
wp option update blogname "Test Site"
|
||||
wp option update permalink_structure "/%postname%/"
|
||||
wp rewrite flush
|
||||
`);
|
||||
|
||||
return {
|
||||
slug,
|
||||
url: `https://${this.config.subsiteDomain}/${slug}`,
|
||||
adminUrl: `https://${this.config.subsiteDomain}/${slug}/wp-admin`
|
||||
};
|
||||
}
|
||||
|
||||
async deleteTestSubsite(slug) {
|
||||
await this.runWpCli(`wp site delete --slug=${slug} --yes`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Dependency Management
|
||||
|
||||
### Installing Required Plugins
|
||||
|
||||
The AI can specify dependencies that must be present:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"required_plugins": [
|
||||
{
|
||||
"plugin_slug": "woocommerce",
|
||||
"version": ">=8.0",
|
||||
"source": "wordpress.org",
|
||||
"activate": true
|
||||
},
|
||||
{
|
||||
"plugin_slug": "advanced-custom-fields",
|
||||
"version": "6.2.0",
|
||||
"source": "wordpress.org",
|
||||
"activate": true
|
||||
},
|
||||
{
|
||||
"plugin_slug": "custom-premium-plugin",
|
||||
"source": "url",
|
||||
"source_url": "https://example.com/plugins/custom.zip",
|
||||
"activate": true
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Installation Process:**
|
||||
```javascript
|
||||
class PluginInstaller {
|
||||
async installPlugin(subsiteUrl, pluginSpec) {
|
||||
const { plugin_slug, source, source_url, activate } = pluginSpec;
|
||||
|
||||
switch (source) {
|
||||
case 'wordpress.org':
|
||||
await this.runWpCli(`
|
||||
wp --url=${subsiteUrl} plugin install ${plugin_slug} --activate
|
||||
`);
|
||||
break;
|
||||
|
||||
case 'url':
|
||||
// Download and install from URL
|
||||
await this.runWpCli(`
|
||||
wp --url=${subsiteUrl} plugin install ${source_url} --activate
|
||||
`);
|
||||
break;
|
||||
|
||||
case 'local':
|
||||
// Copy from local path to subsite
|
||||
const targetPath = `${this.config.wpPath}/wp-content/plugins/${plugin_slug}`;
|
||||
await this.ssh.copyFile(source_url, targetPath);
|
||||
if (activate) {
|
||||
await this.runWpCli(`
|
||||
wp --url=${subsiteUrl} plugin activate ${plugin_slug}
|
||||
`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Queue Management for High Concurrency
|
||||
|
||||
### Session Queue System
|
||||
|
||||
**Challenge:** Even with multisite, some operations (like subsite creation) can have race conditions.
|
||||
|
||||
**Solution:** Queue system with parallel execution lanes
|
||||
|
||||
```javascript
|
||||
class TestQueue {
|
||||
constructor(maxConcurrent = 20) {
|
||||
this.maxConcurrent = maxConcurrent;
|
||||
this.activeSessions = new Map();
|
||||
this.pendingQueue = [];
|
||||
this.lanes = new Array(maxConcurrent).fill(null); // Execution lanes
|
||||
}
|
||||
|
||||
async enqueue(testRequest) {
|
||||
const sessionId = this.generateSessionId();
|
||||
|
||||
// Find available lane
|
||||
const laneIndex = this.findAvailableLane();
|
||||
|
||||
if (laneIndex === -1) {
|
||||
// All lanes busy, add to pending
|
||||
return new Promise((resolve) => {
|
||||
this.pendingQueue.push({
|
||||
sessionId,
|
||||
request: testRequest,
|
||||
resolve,
|
||||
enqueuedAt: Date.now()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Execute immediately in available lane
|
||||
return this.executeInLane(laneIndex, sessionId, testRequest);
|
||||
}
|
||||
|
||||
async executeInLane(laneIndex, sessionId, request) {
|
||||
this.lanes[laneIndex] = sessionId;
|
||||
|
||||
try {
|
||||
const result = await this.runTest(sessionId, request);
|
||||
return result;
|
||||
} finally {
|
||||
this.lanes[laneIndex] = null;
|
||||
this.processQueue();
|
||||
}
|
||||
}
|
||||
|
||||
processQueue() {
|
||||
if (this.pendingQueue.length === 0) return;
|
||||
|
||||
const laneIndex = this.findAvailableLane();
|
||||
if (laneIndex === -1) return;
|
||||
|
||||
const next = this.pendingQueue.shift();
|
||||
|
||||
// Check for timeout
|
||||
if (Date.now() - next.enqueuedAt > TEST_CONFIG.queueTimeout) {
|
||||
next.resolve({
|
||||
ok: false,
|
||||
error: 'Test queue timeout - too many concurrent tests'
|
||||
});
|
||||
this.processQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
this.executeInLane(laneIndex, next.sessionId, next.request)
|
||||
.then(next.resolve)
|
||||
.catch(error => next.resolve({ ok: false, error: error.message }));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Workflow
|
||||
|
||||
### Step-by-Step Process
|
||||
|
||||
```
|
||||
User Request
|
||||
↓
|
||||
AI Generates Plugin Code
|
||||
↓
|
||||
AI Calls test_plugin_external_wp
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 1. QUEUE & PREPARE │
|
||||
│ - Add to test queue │
|
||||
│ - Wait for available lane │
|
||||
│ - Generate session ID │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 2. PROVISION SUBSITE │
|
||||
│ - Create multisite subsite │
|
||||
│ - Configure permalinks │
|
||||
│ - Set up test database tables │
|
||||
│ Duration: 3-5 seconds │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 3. INSTALL DEPENDENCIES │
|
||||
│ - Install WordPress (if needed) │
|
||||
│ - Install WooCommerce │
|
||||
│ - Install other required plugins │
|
||||
│ - Activate all │
|
||||
│ Duration: 10-30 seconds │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 4. DEPLOY TEST PLUGIN │
|
||||
│ - Copy plugin files via SFTP │
|
||||
│ - Activate plugin │
|
||||
│ - Check for activation errors │
|
||||
│ Duration: 2-5 seconds │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 5. RUN TESTS │
|
||||
│ CLI Mode: │
|
||||
│ - WP-CLI verification commands │
|
||||
│ - HTTP endpoint checks │
|
||||
│ - Database state validation │
|
||||
│ - Error log scanning │
|
||||
│ │
|
||||
│ Visual Mode (if enabled): │
|
||||
│ - Launch headless browser │
|
||||
│ - Navigate to test URLs │
|
||||
│ - Execute user interactions │
|
||||
│ - Capture screenshots │
|
||||
│ - Check JavaScript console │
|
||||
│ Duration: 10-60 seconds │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 6. EVALUATE & AUTO-FIX (if needed) │
|
||||
│ IF tests failed AND auto_fix=true:│
|
||||
│ - Analyze failure patterns │
|
||||
│ - Generate fix suggestions │
|
||||
│ - Modify plugin code │
|
||||
│ - Re-deploy and re-test │
|
||||
│ - Max 3 attempts │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 7. RETURN RESULTS │
|
||||
│ - Compile test results │
|
||||
│ - Include screenshots (visual) │
|
||||
│ - Provide fix recommendations │
|
||||
│ - Schedule cleanup │
|
||||
└─────────────────────────────────────┘
|
||||
↓
|
||||
AI Receives Results
|
||||
↓
|
||||
IF passed → Complete task
|
||||
IF failed → Fix issues and re-call tool
|
||||
↓
|
||||
┌─────────────────────────────────────┐
|
||||
│ 8. CLEANUP (async, after delay) │
|
||||
│ - Delete subsite │
|
||||
│ - Remove plugin files │
|
||||
│ - Clear test data │
|
||||
│ - Free up lane │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Error Handling & Auto-Fix
|
||||
|
||||
### Common Failures & Fixes
|
||||
|
||||
```javascript
|
||||
const AUTO_FIX_PATTERNS = [
|
||||
{
|
||||
pattern: /Rewrite rules not working/i,
|
||||
check: async (subsite) => {
|
||||
const rules = await runWpCli(`wp --url=${subsite} rewrite list`);
|
||||
return !rules.includes('custom-login');
|
||||
},
|
||||
fix: async (pluginPath) => {
|
||||
// Add flush_rewrite_rules() to activation hook
|
||||
await editFile(pluginPath + '/includes/activation.php',
|
||||
'function activate_plugin() {',
|
||||
'function activate_plugin() {\n flush_rewrite_rules();'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
pattern: /Undefined function.*wc_/i,
|
||||
check: async (subsite) => {
|
||||
const logs = await runWpCli(`tail -n 20 /var/log/wp-errors.log`);
|
||||
return logs.includes('Call to undefined function wc_');
|
||||
},
|
||||
fix: async (pluginPath) => {
|
||||
// Add WooCommerce dependency check
|
||||
await editFile(pluginPath + '/main.php',
|
||||
'<?php',
|
||||
'<?php\n// Check WooCommerce is active\nif (!in_array(\'woocommerce/woocommerce.php\', apply_filters(\'active_plugins\', get_option(\'active_plugins\')))) {\n return;\n}'
|
||||
);
|
||||
}
|
||||
},
|
||||
{
|
||||
pattern: /CSS not loading/i,
|
||||
check: async (subsite) => {
|
||||
const html = await runWpCli(`wp --url=${subsite} eval 'echo wp_remote_retrieve_body(wp_remote_get(home_url("/custom-login")));'`);
|
||||
return !html.includes('custom-login.css');
|
||||
},
|
||||
fix: async (pluginPath) => {
|
||||
// Ensure CSS is enqueued
|
||||
await editFile(pluginPath + '/includes/enqueue.php',
|
||||
'',
|
||||
'wp_enqueue_style(\'custom-login-css\', plugins_url(\'css/custom-login.css\', __FILE__));'
|
||||
);
|
||||
}
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration & Environment Variables
|
||||
|
||||
### Required Environment Variables
|
||||
|
||||
```bash
|
||||
# WordPress Test Site Connection
|
||||
TEST_WP_HOST=testsite.example.com
|
||||
TEST_WP_SSH_USER=wordpress
|
||||
TEST_WP_SSH_KEY_PATH=/path/to/ssh/key
|
||||
TEST_WP_PATH=/var/www/html
|
||||
|
||||
# Multisite Configuration
|
||||
TEST_WP_MULTISITE=true
|
||||
TEST_WP_SUBSITE_DOMAIN=testsite.example.com
|
||||
|
||||
# Testing Configuration
|
||||
TEST_ENABLE_VISUAL=true
|
||||
TEST_VISUAL_BROWSER=chromium
|
||||
TEST_MAX_CONCURRENT=20
|
||||
TEST_TIMEOUT=300
|
||||
TEST_AUTO_CLEANUP=true
|
||||
TEST_CLEANUP_DELAY=3600
|
||||
|
||||
# Optional: Browser Testing (for visual mode)
|
||||
TEST_BROWSER_HEADLESS=true
|
||||
TEST_BROWSER_TIMEOUT=30000
|
||||
TEST_SCREENSHOT_PATH=/var/results/screenshots
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1: Core Infrastructure (Week 1)
|
||||
- [ ] Set up multisite WordPress
|
||||
- [ ] Implement SSH/SFTP connection layer
|
||||
- [ ] Create `test_plugin_external_wp` tool
|
||||
- [ ] Build subsite provisioning system
|
||||
- [ ] Basic CLI test mode
|
||||
|
||||
### Phase 2: Testing Modes (Week 2)
|
||||
- [ ] Implement CLI test scenarios
|
||||
- [ ] Add visual testing with Playwright
|
||||
- [ ] Create queue management system
|
||||
- [ ] Build dependency installer
|
||||
|
||||
### Phase 3: Auto-Fix & Polish (Week 3)
|
||||
- [ ] Implement auto-fix patterns
|
||||
- [ ] Add retry logic
|
||||
- [ ] Error classification
|
||||
- [ ] Result formatting for AI
|
||||
|
||||
### Phase 4: Production Ready (Week 4)
|
||||
- [ ] Concurrent session testing
|
||||
- [ ] Performance optimization
|
||||
- [ ] Documentation
|
||||
- [ ] Monitoring & alerting
|
||||
|
||||
---
|
||||
|
||||
## Example Usage in AI Workflow
|
||||
|
||||
### Example 1: Custom Login Plugin
|
||||
|
||||
**User:** "Create a plugin that adds a custom login page at /vendor-login"
|
||||
|
||||
**AI Actions:**
|
||||
1. Generates plugin code
|
||||
2. Calls `test_plugin_external_wp`:
|
||||
|
||||
```javascript
|
||||
{
|
||||
"plugin_path": "./vendor-login-plugin",
|
||||
"test_mode": "both",
|
||||
"required_plugins": [],
|
||||
"test_scenarios": [
|
||||
{
|
||||
"name": "Endpoint accessible",
|
||||
"type": "endpoint",
|
||||
"url": "/vendor-login",
|
||||
"assertions": {
|
||||
"status_code": 200,
|
||||
"contains": ["<form", "login"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Rewrite rule exists",
|
||||
"type": "custom",
|
||||
"wp_cli_command": "wp rewrite list --match=/vendor-login",
|
||||
"assertions": {
|
||||
"contains": ["vendor-login"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"visual_tests": [
|
||||
{
|
||||
"name": "Login form renders",
|
||||
"url": "/vendor-login",
|
||||
"actions": [
|
||||
{ "action": "goto", "url": "/vendor-login" },
|
||||
{ "action": "screenshot", "name": "login-page" }
|
||||
],
|
||||
"assertions": {
|
||||
"visible": ["input[name='log']", "input[type='submit']"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Result:**
|
||||
- Subsite created: `https://testsite.example.com/test-a3f7d2`
|
||||
- Plugin installed and activated
|
||||
- Tests run (5 CLI, 3 visual)
|
||||
- Screenshot: `login-page.png` showing working form
|
||||
- AI confirms: "Plugin tested and verified - custom login page working"
|
||||
|
||||
---
|
||||
|
||||
### Example 2: WooCommerce Payment Gateway
|
||||
|
||||
**User:** "Create a WooCommerce payment gateway"
|
||||
|
||||
**AI Actions:**
|
||||
```javascript
|
||||
{
|
||||
"plugin_path": "./custom-gateway",
|
||||
"test_mode": "both",
|
||||
"required_plugins": [
|
||||
{ "plugin_slug": "woocommerce", "activate": true }
|
||||
],
|
||||
"test_scenarios": [
|
||||
{
|
||||
"name": "Gateway registered",
|
||||
"type": "custom",
|
||||
"wp_cli_command": "wp eval 'var_dump(array_keys(WC()->payment_gateways->payment_gateways()));'",
|
||||
"assertions": {
|
||||
"contains": ["custom_gateway"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Checkout loads without errors",
|
||||
"type": "endpoint",
|
||||
"url": "/checkout",
|
||||
"assertions": {
|
||||
"status_code": 200,
|
||||
"not_contains": ["Fatal error", "Warning"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"visual_tests": [
|
||||
{
|
||||
"name": "Gateway appears in checkout",
|
||||
"url": "/checkout",
|
||||
"actions": [
|
||||
{ "action": "goto", "url": "/product/test-product" },
|
||||
{ "action": "click", "selector": ".add_to_cart_button" },
|
||||
{ "action": "wait", "timeout": 2000 },
|
||||
{ "action": "goto", "url": "/checkout" },
|
||||
{ "action": "screenshot", "name": "checkout-gateway" }
|
||||
],
|
||||
"assertions": {
|
||||
"visible": ["#payment_method_custom_gateway"],
|
||||
"no_console_errors": true
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Maintenance
|
||||
|
||||
### Health Checks
|
||||
|
||||
```javascript
|
||||
// Periodic health check
|
||||
async function healthCheck() {
|
||||
const checks = {
|
||||
ssh: await checkSshConnection(),
|
||||
wp_cli: await checkWpCli(),
|
||||
multisite: await checkMultisiteEnabled(),
|
||||
disk_space: await checkDiskSpace(),
|
||||
queue_length: testQueue.pendingQueue.length
|
||||
};
|
||||
|
||||
if (Object.values(checks).some(c => !c.ok)) {
|
||||
await alertAdmin(checks);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Metrics to Track
|
||||
- Test queue length and wait times
|
||||
- Average test duration per mode
|
||||
- Failure rate by plugin type
|
||||
- Auto-fix success rate
|
||||
- Resource usage (disk, memory)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
This system provides:
|
||||
|
||||
1. **Full Isolation:** Each test runs in its own multisite subsite
|
||||
2. **High Concurrency:** Queue system handles 20+ simultaneous tests
|
||||
3. **Two Testing Modes:**
|
||||
- CLI mode: Fast, deterministic WP-CLI testing
|
||||
- Visual mode: Real browser automation with screenshots
|
||||
4. **Automatic Setup:** Provisions subsite, installs dependencies, deploys plugin
|
||||
5. **Auto-Fix:** Attempts to fix common failures automatically
|
||||
6. **Proof of Work:** Returns screenshots, logs, and detailed test results
|
||||
7. **Cleanup:** Automatic teardown after test completion
|
||||
|
||||
**Result:** Users receive plugins that are proven to work on a real WordPress environment, with objective evidence of functionality.
|
||||
Reference in New Issue
Block a user