9.5 KiB
9.5 KiB
External WP-CLI Testing - Implementation Reference
Complete Data Flow
1. User Enables Toggle in Builder
File: chat/public/builder.js
// Line ~36
builderState.externalTestingEnabled = false // default
// User clicks toggle → sets to true
builderState.externalTestingEnabled = true
// When sending message (line ~3967)
const payload = {
content: messageContent,
model,
cli,
externalTestingEnabled: !!builderState.externalTestingEnabled
}
2. Chat Server Receives Message
File: chat/server.js
// Line ~8804
const wpCliMcpEnabled = message?.externalTestingEnabled === true
// Line ~8885-8901: If enabled, inject MCP server
if (wpCliMcpEnabled) {
const wpMcpServerPath = path.resolve(__dirname, '../opencode/mcp-servers/wp-cli-testing/index.js')
executionEnv.OPENCODE_EXTRA_MCP_SERVERS = JSON.stringify([
{
name: 'wp-cli-testing',
command: 'node',
args: [wpMcpServerPath],
disabled: false
}
])
} else {
delete executionEnv.OPENCODE_EXTRA_MCP_SERVERS // Safety gate
}
// Pass env to OpenCode
await opencodeManager.executeInSession(..., { env: executionEnv })
3. OpenCode Reads Environment Variable
File: opencode/packages/opencode/src/config/config.ts
// Line ~233-294: Parse OPENCODE_EXTRA_MCP_SERVERS
const extraMcpRaw = process.env.OPENCODE_EXTRA_MCP_SERVERS
if (extraMcpRaw && typeof extraMcpRaw === "string") {
const parsed = JSON.parse(extraMcpRaw)
if (Array.isArray(parsed)) {
result.mcp ??= {}
for (const entry of parsed) {
result.mcp[entry.name] = {
type: "local",
command: [...commandParts, ...argsParts],
env: envRecord,
enabled: true,
...
}
}
}
}
Result: OpenCode config now has mcp['wp-cli-testing'] configured
4. OpenCode Starts MCP Server
File: opencode/packages/opencode/src/mcp/index.ts
OpenCode's MCP system automatically:
- Reads
config.mcp['wp-cli-testing'] - Spawns:
node opencode/mcp-servers/wp-cli-testing/index.js - Connects via stdio transport
- Calls
listTools()to get available tools
5. MCP Server Registers Tools
File: opencode/mcp-servers/wp-cli-testing/index.js
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "test_plugin_external_wp",
description: "Run WP-CLI based verification...",
inputSchema: { ... }
},
{
name: "external_wp_testing_config",
description: "Return resolved config...",
inputSchema: { ... }
}
]
}
})
6. AI Invokes test_plugin_external_wp Tool
File: opencode/packages/opencode/src/session/prompt.ts
OpenCode's session handler:
- Tool calls go through
resolveTools()(line ~668) - MCP tools added via
MCP.tools()(line ~745) - AI invokes tool with parameters
test_plugin_external_wp({
plugin_path: "/workspace/my-plugin",
plugin_slug: "my-plugin",
test_scenarios: [...]
})
7. MCP Server Handles Tool Call
File: opencode/mcp-servers/wp-cli-testing/index.js
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const toolName = req.params.name
const args = req.params.arguments
// Import external-wp-testing module
const externalTesting = await import("../../../chat/external-wp-testing.js")
const mod = externalTesting.default ?? externalTesting
const tester = mod.createExternalWpTester({})
const result = await tester.runTest(
{
plugin_path: args.plugin_path,
plugin_slug: args.plugin_slug,
...
},
{ configOverrides: {} }
)
return { content: [{ type: "text", text: JSON.stringify(result) }] }
})
8. External WP Testing Executes
File: chat/external-wp-testing.js
async function runTest(input, opts) {
const config = getExternalTestingConfig(opts.configOverrides)
// Queue the test
return queue.enqueue(async () => {
// 1. Create subsite (if multisite enabled)
if (config.enableMultisite) {
subsite = await createSubsite(config, sessionId)
// Runs: wp site create --slug=test-abc123
}
// 2. Upload plugin via SCP
if (pluginPath) {
await installPluginFromLocal(config, pluginPath, pluginSlug, sessionId)
}
// 3. Install required plugins
const installedPlugins = await installRequiredPlugins(config, subsite.url, requiredPlugins)
// 4. Activate test plugin
if (pluginSlug) {
await runSshCommand(config,
buildWpCliCommand(config, subsite.url, `plugin activate ${pluginSlug}`)
)
}
// 5. Run test scenarios
const cliTests = await runTestScenarios(config, subsite.url, scenarios)
// 6. Schedule cleanup
if (config.autoCleanup && subsite.slug) {
setTimeout(() => { deleteSubsite(config, subsite.slug) }, config.cleanupDelayMs)
}
return { ok: true, test_results: { cli_tests: cliTests }, ... }
})
}
9. SSH Commands Execute on WordPress Server
Functions:
buildWpCliCommand(config, url, command)→wp --path=/var/www/html --url=https://site.com/test-abc commandrunSshCommand(config, command)→ Executes via SSH with key auth
Example Commands Executed:
# Create subsite
ssh -i ~/.ssh/wp-key wordpress@wp-test.com "wp --path=/var/www/html site create --slug=test-abc123"
# Copy plugin
scp -i ~/.ssh/wp-key -r /local/plugin wordpress@wp-test.com:/var/www/html/wp-content/plugins/
# Activate plugin
ssh -i ~/.ssh/wp-key wordpress@wp-test.com "wp --path=/var/www/html --url=https://wp-test.com/test-abc123 plugin activate my-plugin"
# Run test
ssh -i ~/.ssh/wp-key wordpress@wp-test.com "wp --path=/var/www/html --url=https://wp-test.com/test-abc123 plugin status my-plugin"
# Later: Delete subsite
ssh -i ~/.ssh/wp-key wordpress@wp-test.com "wp --path=/var/www/html site delete --slug=test-abc123 --yes"
10. Results Return to AI
Return Path:
WordPress Server (via SSH)
↓ command output
external-wp-testing.js runTest()
↓ formatted result
MCP Server CallToolRequestSchema handler
↓ JSON response
OpenCode MCP client
↓ tool result
AI Session Handler
↓ assistant message
Chat Server
↓ SSE stream
Builder UI
Result Format:
{
"ok": true,
"session_id": "abc123",
"subsite_url": "https://wp-test.example.com/test-abc123",
"test_results": {
"mode": "cli",
"cli_tests": {
"passed": 3,
"failed": 0,
"results": [...]
}
},
"installed_plugins": [...],
"duration": 15230,
"cleanup_scheduled": "2026-02-08T11:30:00Z"
}
Key Files
| File | Purpose |
|---|---|
chat/public/builder.js |
Builder UI toggle state |
chat/server.js |
Gate MCP injection via env var |
opencode/packages/opencode/src/config/config.ts |
Parse OPENCODE_EXTRA_MCP_SERVERS |
opencode/packages/opencode/src/mcp/index.ts |
Load and connect MCP servers |
opencode/mcp-servers/wp-cli-testing/index.js |
MCP server implementation |
chat/external-wp-testing.js |
Core testing logic |
Environment Variables Flow
Host Environment
↓
Chat Server process.env
↓
executionEnv = {...process.env, OPENCODE_EXTRA_MCP_SERVERS: "..."}
↓
OpenCode spawned with executionEnv
↓
process.env.TEST_WP_HOST → external-wp-testing.js getExternalTestingConfig()
↓
SSH connection using TEST_WP_SSH_KEY
↓
Commands execute on WordPress server
Required Environment Variables:
TEST_WP_HOSTTEST_WP_SSH_USERTEST_WP_SSH_KEYTEST_WP_PATHTEST_WP_BASE_URL- etc. (see EXTERNAL_WP_CLI_TESTING_SETUP.md)
Security Gates
- Builder Toggle - User must explicitly enable
- Environment Variable Gate - Only set when toggle is on
- MCP Loading - Only loads when env var present
- SSH Key Auth - Requires valid key file
- Tool Gating - Tools not available if toggle off
When toggle is OFF:
externalTestingEnabled: falsein payloadwpCliMcpEnabled = falseon serverOPENCODE_EXTRA_MCP_SERVERSis deleted from env- MCP server never loads
- Tools never available to AI
Testing The Flow
1. Check Environment
echo $TEST_WP_HOST
echo $TEST_WP_SSH_KEY
2. Test SSH Connection
ssh -i $TEST_WP_SSH_KEY $TEST_WP_SSH_USER@$TEST_WP_HOST "wp site list"
3. Enable Toggle in Builder
- Open Builder
- Enable "External WP CLI testing"
- Verify toggle shows enabled state
4. Check Server Logs
tail -f logs/chat-server.log | grep "WP CLI Testing"
# Should see: "Enabling WP CLI Testing MCP server"
5. Ask AI to Test
"Create a plugin and test it on the external WordPress server"
6. Verify Subsite Created
ssh -i $TEST_WP_SSH_KEY $TEST_WP_SSH_USER@$TEST_WP_HOST "wp site list | grep test-"
Multisite Architecture
WordPress Multisite Network
├── Main Site (/)
│ └── Core WP, Plugins, Themes (shared)
├── Subsite: /test-abc123 (Session 1)
│ ├── Isolated content/options
│ ├── Test Plugin A activated
│ └── Tests running
├── Subsite: /test-def456 (Session 2)
│ ├── Isolated content/options
│ ├── Test Plugin B activated
│ └── Tests running
└── Subsite: /test-xyz789 (Session 3)
└── ...
Concurrency: 20+ simultaneous tests via queue system + isolated subsites
Cleanup: Each subsite auto-deleted after 1 hour (configurable via TEST_CLEANUP_DELAY)