533 lines
20 KiB
JavaScript
533 lines
20 KiB
JavaScript
const fs = require('fs/promises');
|
|
const fsSync = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const { spawn } = require('child_process');
|
|
const { randomUUID } = require('crypto');
|
|
|
|
const DEFAULT_CONFIG = {
|
|
wpHost: process.env.TEST_WP_HOST || process.env.EXTERNAL_WP_HOST || '',
|
|
wpSshUser: process.env.TEST_WP_SSH_USER || process.env.EXTERNAL_WP_SSH_USER || 'wordpress',
|
|
wpSshKey: process.env.TEST_WP_SSH_KEY || process.env.TEST_WP_SSH_KEY_PATH || process.env.EXTERNAL_WP_SSH_KEY || '',
|
|
wpPath: process.env.TEST_WP_PATH || process.env.EXTERNAL_WP_PATH || '/var/www/html',
|
|
wpBaseUrl: process.env.TEST_WP_BASE_URL || process.env.EXTERNAL_WP_BASE_URL || '',
|
|
enableMultisite: (process.env.TEST_WP_MULTISITE || process.env.EXTERNAL_WP_MULTISITE || 'true') !== 'false',
|
|
subsitePrefix: process.env.TEST_WP_SUBSITE_PREFIX || process.env.EXTERNAL_WP_SUBSITE_PREFIX || 'test',
|
|
subsiteDomain: process.env.TEST_WP_SUBSITE_DOMAIN || process.env.EXTERNAL_WP_SUBSITE_DOMAIN || '',
|
|
subsiteMode: (process.env.TEST_WP_SUBSITE_MODE || process.env.EXTERNAL_WP_SUBSITE_MODE || 'subdirectory').toLowerCase(),
|
|
maxConcurrentTests: Number(process.env.TEST_MAX_CONCURRENT || process.env.EXTERNAL_WP_MAX_CONCURRENT || 20),
|
|
queueTimeoutMs: Number(process.env.TEST_QUEUE_TIMEOUT || process.env.EXTERNAL_WP_QUEUE_TIMEOUT || 300000),
|
|
testTimeoutMs: Number(process.env.TEST_TIMEOUT || process.env.EXTERNAL_WP_TEST_TIMEOUT || 600000),
|
|
autoCleanup: (process.env.TEST_AUTO_CLEANUP || process.env.EXTERNAL_WP_AUTO_CLEANUP || 'true') !== 'false',
|
|
cleanupDelayMs: Number(process.env.TEST_CLEANUP_DELAY || process.env.EXTERNAL_WP_CLEANUP_DELAY || 3600000),
|
|
errorLogPath: process.env.TEST_WP_ERROR_LOG || process.env.EXTERNAL_WP_ERROR_LOG || '/var/log/wp-errors.log',
|
|
strictHostKeyChecking: (process.env.TEST_SSH_STRICT || process.env.EXTERNAL_WP_SSH_STRICT || 'false') === 'true',
|
|
};
|
|
|
|
function expandHome(p) {
|
|
if (!p || typeof p !== 'string') return p;
|
|
if (p.startsWith('~')) {
|
|
return path.join(os.homedir(), p.slice(1));
|
|
}
|
|
return p;
|
|
}
|
|
|
|
function getExternalTestingConfig(overrides = {}) {
|
|
const config = { ...DEFAULT_CONFIG, ...(overrides || {}) };
|
|
config.wpSshKey = expandHome(config.wpSshKey);
|
|
config.wpBaseUrl = (config.wpBaseUrl || '').trim();
|
|
if (!config.wpBaseUrl && config.wpHost) {
|
|
config.wpBaseUrl = `https://${config.wpHost}`;
|
|
}
|
|
config.maxConcurrentTests = Number.isFinite(config.maxConcurrentTests) && config.maxConcurrentTests > 0
|
|
? Math.floor(config.maxConcurrentTests)
|
|
: 1;
|
|
config.queueTimeoutMs = Number.isFinite(config.queueTimeoutMs) && config.queueTimeoutMs > 0
|
|
? Math.floor(config.queueTimeoutMs)
|
|
: 300000;
|
|
config.testTimeoutMs = Number.isFinite(config.testTimeoutMs) && config.testTimeoutMs > 0
|
|
? Math.floor(config.testTimeoutMs)
|
|
: 600000;
|
|
config.cleanupDelayMs = Number.isFinite(config.cleanupDelayMs) && config.cleanupDelayMs >= 0
|
|
? Math.floor(config.cleanupDelayMs)
|
|
: 3600000;
|
|
config.subsiteMode = config.subsiteMode === 'subdomain' ? 'subdomain' : 'subdirectory';
|
|
return config;
|
|
}
|
|
|
|
function spawnCommand(command, args, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const start = Date.now();
|
|
const child = spawn(command, args, { ...options, windowsHide: true });
|
|
let stdout = '';
|
|
let stderr = '';
|
|
|
|
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
|
|
child.on('error', (error) => {
|
|
reject(error);
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
resolve({ stdout, stderr, code: code ?? 0, durationMs: Date.now() - start });
|
|
});
|
|
});
|
|
}
|
|
|
|
function buildSshBaseArgs(config) {
|
|
const args = [];
|
|
if (config.wpSshKey) {
|
|
args.push('-i', config.wpSshKey);
|
|
}
|
|
if (!config.strictHostKeyChecking) {
|
|
args.push('-o', 'StrictHostKeyChecking=no');
|
|
}
|
|
args.push('-o', 'BatchMode=yes');
|
|
return args;
|
|
}
|
|
|
|
async function runSshCommand(config, command, options = {}) {
|
|
const target = `${config.wpSshUser}@${config.wpHost}`;
|
|
const args = [...buildSshBaseArgs(config), target, command];
|
|
return spawnCommand('ssh', args, { timeout: options.timeout || config.testTimeoutMs });
|
|
}
|
|
|
|
async function runScpUpload(config, localPath, remotePath) {
|
|
const args = [...buildSshBaseArgs(config), '-r', localPath, `${config.wpSshUser}@${config.wpHost}:${remotePath}`];
|
|
return spawnCommand('scp', args, { timeout: config.testTimeoutMs });
|
|
}
|
|
|
|
function buildWpCliCommand(config, url, command) {
|
|
return `wp --path=${config.wpPath} --url=${url} ${command}`;
|
|
}
|
|
|
|
function resolveSubsiteUrl(config, slug) {
|
|
if (!config.enableMultisite) return config.wpBaseUrl;
|
|
const domain = config.subsiteDomain || config.wpHost;
|
|
if (config.subsiteMode === 'subdomain') {
|
|
return `https://${slug}.${domain}`;
|
|
}
|
|
return `${config.wpBaseUrl.replace(/\/$/, '')}/${slug}`;
|
|
}
|
|
|
|
async function createSubsite(config, sessionId) {
|
|
const slug = `${config.subsitePrefix}-${sessionId.slice(0, 8)}`;
|
|
const title = `Test Environment ${sessionId}`;
|
|
const adminEmail = 'test@example.com';
|
|
|
|
const createCmd = buildWpCliCommand(
|
|
config,
|
|
config.wpBaseUrl,
|
|
`site create --slug=${slug} --title="${title}" --email=${adminEmail}`
|
|
);
|
|
const createRes = await runSshCommand(config, createCmd, { timeout: config.testTimeoutMs });
|
|
|
|
if (createRes.code !== 0) {
|
|
throw new Error(`Failed to create subsite: ${createRes.stderr || createRes.stdout}`);
|
|
}
|
|
|
|
const url = resolveSubsiteUrl(config, slug);
|
|
|
|
// Configure permalink structure
|
|
const permalinkCmd = buildWpCliCommand(
|
|
config,
|
|
url,
|
|
'option update permalink_structure "/%postname%/"'
|
|
);
|
|
await runSshCommand(config, permalinkCmd, { timeout: config.testTimeoutMs });
|
|
|
|
// Flush rewrite rules
|
|
const rewriteCmd = buildWpCliCommand(
|
|
config,
|
|
url,
|
|
'rewrite flush'
|
|
);
|
|
await runSshCommand(config, rewriteCmd, { timeout: config.testTimeoutMs });
|
|
|
|
return { slug, url, adminUrl: `${url.replace(/\/$/, '')}/wp-admin` };
|
|
}
|
|
|
|
async function deleteSubsite(config, slug) {
|
|
if (!slug) return;
|
|
const deleteCmd = buildWpCliCommand(
|
|
config,
|
|
config.wpBaseUrl,
|
|
`site delete --slug=${slug} --yes`
|
|
);
|
|
await runSshCommand(config, deleteCmd, { timeout: config.testTimeoutMs });
|
|
}
|
|
|
|
async function installPluginFromLocal(config, pluginPath, pluginSlug, sessionId) {
|
|
if (!pluginPath || !pluginSlug) throw new Error('Plugin path and slug are required');
|
|
const safeSlug = pluginSlug.replace(/[^a-zA-Z0-9\-_]/g, '');
|
|
const remoteBase = path.posix.join(config.wpPath, 'wp-content', 'plugins');
|
|
const tmpDir = path.posix.join(remoteBase, `${safeSlug}__tmp_${sessionId}`);
|
|
const finalDir = path.posix.join(remoteBase, safeSlug);
|
|
|
|
const cleanupTmp = async () => {
|
|
const cmd = `rm -rf ${tmpDir}`;
|
|
await runSshCommand(config, cmd, { timeout: config.testTimeoutMs }).catch(() => {});
|
|
};
|
|
|
|
await cleanupTmp();
|
|
const scpResult = await runScpUpload(config, pluginPath, tmpDir);
|
|
if (scpResult.code !== 0) {
|
|
throw new Error(`Failed to upload plugin: ${scpResult.stderr || scpResult.stdout}`);
|
|
}
|
|
|
|
const moveCmd = `rm -rf ${finalDir} && mv ${tmpDir} ${finalDir}`;
|
|
const moveRes = await runSshCommand(config, moveCmd, { timeout: config.testTimeoutMs });
|
|
if (moveRes.code !== 0) {
|
|
throw new Error(`Failed to install plugin: ${moveRes.stderr || moveRes.stdout}`);
|
|
}
|
|
}
|
|
|
|
async function installRequiredPlugins(config, subsiteUrl, plugins) {
|
|
if (!Array.isArray(plugins)) return [];
|
|
const installed = [];
|
|
for (const spec of plugins) {
|
|
if (!spec || !spec.plugin_slug) continue;
|
|
const slug = spec.plugin_slug;
|
|
const source = (spec.source || 'wordpress.org').toLowerCase();
|
|
const activate = spec.activate !== false;
|
|
let command;
|
|
if (source === 'url' && spec.source_url) {
|
|
command = `plugin install ${spec.source_url} ${activate ? '--activate' : ''}`;
|
|
} else if (source === 'local' && spec.source_url) {
|
|
await installPluginFromLocal(config, spec.source_url, slug, randomUUID());
|
|
command = activate ? `plugin activate ${slug}` : `plugin deactivate ${slug}`;
|
|
} else {
|
|
command = `plugin install ${slug} ${activate ? '--activate' : ''}`;
|
|
}
|
|
|
|
const res = await runSshCommand(config, buildWpCliCommand(config, subsiteUrl, command));
|
|
installed.push({ slug, status: res.code === 0 ? (activate ? 'active' : 'installed') : 'error', output: res.stdout || res.stderr });
|
|
}
|
|
return installed;
|
|
}
|
|
|
|
function normalizeScenarioAssertions(assertions = {}) {
|
|
return {
|
|
status_code: Number.isFinite(assertions.status_code) ? assertions.status_code : null,
|
|
contains: Array.isArray(assertions.contains) ? assertions.contains : [],
|
|
not_contains: Array.isArray(assertions.not_contains) ? assertions.not_contains : [],
|
|
wp_cli_success: assertions.wp_cli_success === true,
|
|
};
|
|
}
|
|
|
|
function normalizeTestScenarios(spec, pluginSlug, config) {
|
|
const scenarios = Array.isArray(spec?.test_scenarios) ? spec.test_scenarios : [];
|
|
const normalized = scenarios.map((s) => ({
|
|
name: String(s?.name || 'Unnamed test'),
|
|
type: String(s?.type || 'custom'),
|
|
url: s?.url || '',
|
|
selector: s?.selector || '',
|
|
expected_text: s?.expected_text || '',
|
|
wp_cli_command: s?.wp_cli_command || '',
|
|
shortcode: s?.shortcode || '',
|
|
hook: s?.hook || '',
|
|
ajax_action: s?.ajax_action || '',
|
|
method: s?.method || '',
|
|
body: s?.body || '',
|
|
assertions: normalizeScenarioAssertions(s?.assertions || {})
|
|
}));
|
|
|
|
if (!normalized.length && pluginSlug) {
|
|
normalized.push({
|
|
name: 'Plugin activates without errors',
|
|
type: 'custom',
|
|
wp_cli_command: `plugin activate ${pluginSlug}`,
|
|
assertions: { wp_cli_success: true, contains: [], not_contains: ['Fatal error', 'Parse error'] }
|
|
});
|
|
normalized.push({
|
|
name: 'Plugin is listed as active',
|
|
type: 'custom',
|
|
wp_cli_command: `plugin status ${pluginSlug}`,
|
|
assertions: { contains: ['Active'] }
|
|
});
|
|
}
|
|
|
|
if (spec?.auto_error_log_check !== false && config?.errorLogPath) {
|
|
const safePath = config.errorLogPath.replace(/"/g, '').trim();
|
|
const grepSlug = pluginSlug ? ` | grep -i "${pluginSlug}"` : '';
|
|
normalized.push({
|
|
name: 'Scan error log for PHP errors',
|
|
type: 'custom',
|
|
wp_cli_command: `eval "echo shell_exec('tail -n 200 ${safePath.replace(/'/g, "'\\''")}${grepSlug} || echo No errors found');"`,
|
|
assertions: { not_contains: ['Fatal error', 'Parse error'] }
|
|
});
|
|
}
|
|
|
|
return normalized;
|
|
}
|
|
|
|
async function runHttpScenario(config, subsiteUrl, scenario, baseFn) {
|
|
const url = scenario.url || '/';
|
|
const method = String(scenario.method || '').toUpperCase() || 'GET';
|
|
const body = scenario.body || '';
|
|
const bodyArg = body ? `, ["body" => ${JSON.stringify(body)}]` : '';
|
|
const requestFn = method === 'POST' ? 'wp_remote_post' : 'wp_remote_get';
|
|
const phpUrl = `${baseFn}("${url}")`;
|
|
const command = buildWpCliCommand(
|
|
config,
|
|
subsiteUrl,
|
|
`eval ' $res = ${requestFn}(${phpUrl}${bodyArg}); echo json_encode(["status" => wp_remote_retrieve_response_code($res), "body" => wp_remote_retrieve_body($res)]);'`
|
|
);
|
|
const result = await runSshCommand(config, command, { timeout: config.testTimeoutMs });
|
|
let statusCode = null;
|
|
let bodyOut = result.stdout || '';
|
|
try {
|
|
const parsed = JSON.parse((result.stdout || '').trim());
|
|
statusCode = parsed.status;
|
|
bodyOut = parsed.body || '';
|
|
} catch (_) {
|
|
bodyOut = result.stdout || result.stderr || '';
|
|
}
|
|
return { result, statusCode, body: bodyOut };
|
|
}
|
|
|
|
function evaluateAssertions(output, statusCode, assertions) {
|
|
const failures = [];
|
|
if (Number.isFinite(assertions.status_code) && statusCode !== assertions.status_code) {
|
|
failures.push(`status_code expected ${assertions.status_code} but got ${statusCode}`);
|
|
}
|
|
for (const needle of assertions.contains || []) {
|
|
if (!output.includes(needle)) {
|
|
failures.push(`missing text: ${needle}`);
|
|
}
|
|
}
|
|
for (const needle of assertions.not_contains || []) {
|
|
if (output.includes(needle)) {
|
|
failures.push(`unexpected text: ${needle}`);
|
|
}
|
|
}
|
|
return failures;
|
|
}
|
|
|
|
async function runTestScenario(config, subsiteUrl, scenario) {
|
|
const assertions = normalizeScenarioAssertions(scenario.assertions || {});
|
|
let output = '';
|
|
let statusCode = null;
|
|
let command = scenario.wp_cli_command || '';
|
|
let result;
|
|
|
|
if (scenario.type === 'endpoint' && !command) {
|
|
const endpointRes = await runHttpScenario(config, subsiteUrl, scenario, 'home_url');
|
|
result = endpointRes.result;
|
|
statusCode = endpointRes.statusCode;
|
|
output = endpointRes.body || '';
|
|
} else if (scenario.type === 'admin_page' && !command) {
|
|
const adminUrl = scenario.url || 'admin.php';
|
|
const adminScenario = { ...scenario, url: adminUrl };
|
|
const adminRes = await runHttpScenario(config, subsiteUrl, adminScenario, 'admin_url');
|
|
result = adminRes.result;
|
|
statusCode = adminRes.statusCode;
|
|
output = adminRes.body || '';
|
|
} else if (scenario.type === 'ajax' && !command) {
|
|
const action = scenario.ajax_action || (scenario.url || '').replace(/^\/?/, '');
|
|
const ajaxUrl = action.includes('admin-ajax.php') || action.includes('action=')
|
|
? action
|
|
: `admin-ajax.php?action=${action || 'ping'}`;
|
|
const ajaxScenario = { ...scenario, url: ajaxUrl, method: scenario.method || 'POST' };
|
|
const ajaxRes = await runHttpScenario(config, subsiteUrl, ajaxScenario, 'admin_url');
|
|
result = ajaxRes.result;
|
|
statusCode = ajaxRes.statusCode;
|
|
output = ajaxRes.body || '';
|
|
} else if (scenario.type === 'shortcode' && !command) {
|
|
const shortcode = scenario.shortcode || scenario.url || '';
|
|
const shortcodeInput = shortcode.includes('[') ? shortcode : `[${shortcode}]`;
|
|
command = `eval "echo do_shortcode('${shortcodeInput.replace(/'/g, "'\\''")}');"`;
|
|
result = await runSshCommand(config, buildWpCliCommand(config, subsiteUrl, command));
|
|
output = (result.stdout || result.stderr || '').trim();
|
|
} else if (scenario.type === 'hook' && !command) {
|
|
const hook = scenario.hook || scenario.url || '';
|
|
command = `eval "echo json_encode([\"action\" => has_action('${hook.replace(/'/g, "'\\''")}') ? 1 : 0, \"filter\" => has_filter('${hook.replace(/'/g, "'\\''")}') ? 1 : 0]);"`;
|
|
result = await runSshCommand(config, buildWpCliCommand(config, subsiteUrl, command));
|
|
output = (result.stdout || result.stderr || '').trim();
|
|
} else {
|
|
if (!command) {
|
|
command = 'eval "echo \'No command provided\';"';
|
|
}
|
|
result = await runSshCommand(config, buildWpCliCommand(config, subsiteUrl, command));
|
|
output = (result.stdout || result.stderr || '').trim();
|
|
}
|
|
|
|
const failures = evaluateAssertions(output, statusCode, assertions);
|
|
if (assertions.wp_cli_success && result.code !== 0) {
|
|
failures.push(`wp_cli_command exited with ${result.code}`);
|
|
}
|
|
|
|
return {
|
|
name: scenario.name,
|
|
status: failures.length ? 'failed' : 'passed',
|
|
command,
|
|
output,
|
|
statusCode,
|
|
duration: result.durationMs || 0,
|
|
failures,
|
|
};
|
|
}
|
|
|
|
async function runTestScenarios(config, subsiteUrl, scenarios) {
|
|
const results = [];
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
for (const scenario of scenarios) {
|
|
const result = await runTestScenario(config, subsiteUrl, scenario);
|
|
results.push(result);
|
|
if (result.status === 'passed') {
|
|
passed += 1;
|
|
} else {
|
|
failed += 1;
|
|
}
|
|
}
|
|
|
|
return { passed, failed, results };
|
|
}
|
|
|
|
class ExternalTestQueue {
|
|
constructor(maxConcurrent, queueTimeoutMs) {
|
|
this.maxConcurrent = maxConcurrent;
|
|
this.queueTimeoutMs = queueTimeoutMs;
|
|
this.active = 0;
|
|
this.queue = [];
|
|
}
|
|
|
|
enqueue(task) {
|
|
return new Promise((resolve) => {
|
|
const entry = { task, resolve, enqueuedAt: Date.now() };
|
|
this.queue.push(entry);
|
|
this.process();
|
|
});
|
|
}
|
|
|
|
process() {
|
|
if (this.active >= this.maxConcurrent) return;
|
|
const entry = this.queue.shift();
|
|
if (!entry) return;
|
|
|
|
if (Date.now() - entry.enqueuedAt > this.queueTimeoutMs) {
|
|
entry.resolve({ ok: false, error: 'Test queue timeout - too many concurrent tests' });
|
|
this.process();
|
|
return;
|
|
}
|
|
|
|
this.active += 1;
|
|
Promise.resolve()
|
|
.then(entry.task)
|
|
.then((result) => entry.resolve(result))
|
|
.catch((error) => entry.resolve({ ok: false, error: error.message || String(error) }))
|
|
.finally(() => {
|
|
this.active -= 1;
|
|
this.process();
|
|
});
|
|
}
|
|
}
|
|
|
|
function createExternalWpTester(options = {}) {
|
|
const logger = typeof options.logger === 'function' ? options.logger : null;
|
|
const queue = new ExternalTestQueue(
|
|
DEFAULT_CONFIG.maxConcurrentTests,
|
|
DEFAULT_CONFIG.queueTimeoutMs
|
|
);
|
|
|
|
const log = (msg, meta) => {
|
|
if (logger) logger(msg, meta || {});
|
|
};
|
|
|
|
async function runTest(input = {}, opts = {}) {
|
|
const config = getExternalTestingConfig(opts.configOverrides);
|
|
if (!config.wpHost || !config.wpSshUser) {
|
|
return { ok: false, error: 'External WordPress test host is not configured' };
|
|
}
|
|
if (!config.wpSshKey || !fsSync.existsSync(config.wpSshKey)) {
|
|
return { ok: false, error: 'SSH key for external WordPress testing is missing or invalid' };
|
|
}
|
|
|
|
const sessionId = input.session_id || randomUUID();
|
|
const pluginPath = input.plugin_path;
|
|
const pluginSlug = input.plugin_slug || path.basename(pluginPath || '').replace(/\.zip$/i, '');
|
|
const requiredPlugins = input.required_plugins || [];
|
|
|
|
return queue.enqueue(async () => {
|
|
const start = Date.now();
|
|
let subsite = { slug: null, url: config.wpBaseUrl, adminUrl: `${config.wpBaseUrl.replace(/\/$/, '')}/wp-admin` };
|
|
const warnings = [];
|
|
const errors = [];
|
|
|
|
try {
|
|
if (config.enableMultisite) {
|
|
subsite = await createSubsite(config, sessionId);
|
|
}
|
|
|
|
if (pluginPath && pluginSlug) {
|
|
await installPluginFromLocal(config, pluginPath, pluginSlug, sessionId);
|
|
} else {
|
|
warnings.push('Plugin path or slug missing; skipping plugin upload');
|
|
}
|
|
|
|
const installedPlugins = await installRequiredPlugins(config, subsite.url, requiredPlugins);
|
|
if (pluginSlug) {
|
|
const activateCmd = buildWpCliCommand(config, subsite.url, `plugin activate ${pluginSlug}`);
|
|
const activateRes = await runSshCommand(config, activateCmd, { timeout: config.testTimeoutMs });
|
|
if (activateRes.code !== 0) {
|
|
errors.push(`Failed to activate plugin ${pluginSlug}: ${activateRes.stderr || activateRes.stdout}`);
|
|
}
|
|
}
|
|
|
|
const scenarios = normalizeTestScenarios(input, pluginSlug, config);
|
|
const cliTests = await runTestScenarios(config, subsite.url, scenarios);
|
|
|
|
const result = {
|
|
ok: cliTests.failed === 0,
|
|
session_id: sessionId,
|
|
subsite_url: subsite.url,
|
|
test_results: {
|
|
mode: 'cli',
|
|
cli_tests: cliTests,
|
|
},
|
|
installed_plugins: installedPlugins,
|
|
errors,
|
|
warnings,
|
|
duration: Date.now() - start,
|
|
};
|
|
|
|
if (config.autoCleanup && subsite.slug) {
|
|
setTimeout(() => {
|
|
deleteSubsite(config, subsite.slug).catch(() => {});
|
|
}, config.cleanupDelayMs);
|
|
result.cleanup_scheduled = new Date(Date.now() + config.cleanupDelayMs).toISOString();
|
|
}
|
|
|
|
return result;
|
|
} catch (error) {
|
|
errors.push(error.message || String(error));
|
|
if (config.autoCleanup && subsite.slug) {
|
|
setTimeout(() => {
|
|
deleteSubsite(config, subsite.slug).catch(() => {});
|
|
}, config.cleanupDelayMs);
|
|
}
|
|
return {
|
|
ok: false,
|
|
session_id: sessionId,
|
|
subsite_url: subsite.url,
|
|
test_results: { mode: 'cli', cli_tests: { passed: 0, failed: 1, results: [] } },
|
|
installed_plugins: [],
|
|
errors,
|
|
warnings,
|
|
duration: Date.now() - start,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
return { runTest, getConfig: () => getExternalTestingConfig() };
|
|
}
|
|
|
|
module.exports = {
|
|
createExternalWpTester,
|
|
getExternalTestingConfig,
|
|
};
|