claude-plugin-wordpress-manager 2.12.2 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +8 -3
- package/CHANGELOG.md +55 -0
- package/docs/GUIDE.md +240 -1
- package/docs/VALIDATION.md +341 -0
- package/docs/plans/2026-03-02-content-framework-architecture.md +612 -0
- package/docs/plans/2026-03-02-content-framework-strategic-reflections.md +228 -0
- package/docs/plans/2026-03-02-content-intelligence-phase2.md +560 -0
- package/docs/plans/2026-03-02-content-pipeline-phase1.md +456 -0
- package/docs/plans/2026-03-02-editorial-calendar-phase3.md +490 -0
- package/docs/validation/.gitkeep +0 -0
- package/docs/validation/dashboard.html +286 -0
- package/docs/validation/results.json +1705 -0
- package/package.json +12 -3
- package/scripts/run-validation.mjs +1132 -0
- package/servers/wp-rest-bridge/build/server.js +16 -5
- package/servers/wp-rest-bridge/build/tools/index.js +0 -9
- package/servers/wp-rest-bridge/build/tools/plugin-repository.js +23 -31
- package/servers/wp-rest-bridge/build/tools/schema.js +10 -2
- package/servers/wp-rest-bridge/build/tools/unified-content.js +10 -2
- package/servers/wp-rest-bridge/build/wordpress.d.ts +0 -3
- package/servers/wp-rest-bridge/build/wordpress.js +16 -98
- package/servers/wp-rest-bridge/package.json +1 -0
- package/skills/wp-analytics/SKILL.md +153 -0
- package/skills/wp-analytics/references/signals-feed-schema.md +417 -0
- package/skills/wp-content-pipeline/SKILL.md +461 -0
- package/skills/wp-content-pipeline/references/content-brief-schema.md +377 -0
- package/skills/wp-content-pipeline/references/site-config-schema.md +431 -0
- package/skills/wp-editorial-planner/SKILL.md +262 -0
- package/skills/wp-editorial-planner/references/editorial-schema.md +268 -0
|
@@ -0,0 +1,1132 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/run-validation.mjs — MCP Tool Validation Runner
|
|
3
|
+
// Spawns wp-rest-bridge server, connects via MCP SDK, tests all registered tools.
|
|
4
|
+
// Usage:
|
|
5
|
+
// node scripts/run-validation.mjs # interactive mode
|
|
6
|
+
// node scripts/run-validation.mjs --site=opencactus # target specific site
|
|
7
|
+
// node scripts/run-validation.mjs --module=gsc # single module
|
|
8
|
+
// node scripts/run-validation.mjs --include-writes # include write tools
|
|
9
|
+
// node scripts/run-validation.mjs --test-writes # CRUD sequence testing
|
|
10
|
+
// node scripts/run-validation.mjs --test-writes --tier=1 # only Tier 1
|
|
11
|
+
// node scripts/run-validation.mjs --delay=200 # ms between calls
|
|
12
|
+
|
|
13
|
+
import { createRequire } from 'module';
|
|
14
|
+
import { writeFileSync, readFileSync, existsSync, mkdirSync } from 'fs';
|
|
15
|
+
import { resolve, dirname } from 'path';
|
|
16
|
+
import { fileURLToPath } from 'url';
|
|
17
|
+
|
|
18
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
19
|
+
const PROJECT_ROOT = resolve(__dirname, '..');
|
|
20
|
+
const SERVER_DIR = resolve(PROJECT_ROOT, 'servers/wp-rest-bridge');
|
|
21
|
+
const SDK_PATH = resolve(SERVER_DIR, 'node_modules/@modelcontextprotocol/sdk');
|
|
22
|
+
const RESULTS_PATH = resolve(PROJECT_ROOT, 'docs/validation/results.json');
|
|
23
|
+
const VALIDATION_MD_PATH = resolve(PROJECT_ROOT, 'docs/VALIDATION.md');
|
|
24
|
+
const RUNNER_VERSION = '1.2.0';
|
|
25
|
+
|
|
26
|
+
// Dynamic import of MCP SDK from server's node_modules
|
|
27
|
+
const require = createRequire(resolve(SERVER_DIR, 'package.json'));
|
|
28
|
+
|
|
29
|
+
// ── CLI Args ─────────────────────────────────────────────────────────
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const getArg = (name) => {
|
|
32
|
+
const a = args.find(a => a.startsWith(`--${name}=`));
|
|
33
|
+
return a ? a.split('=')[1] : null;
|
|
34
|
+
};
|
|
35
|
+
const hasFlag = (name) => args.includes(`--${name}`);
|
|
36
|
+
|
|
37
|
+
const filterModule = getArg('module');
|
|
38
|
+
const filterSite = getArg('site');
|
|
39
|
+
const includeWrites = hasFlag('include-writes');
|
|
40
|
+
const testWrites = hasFlag('test-writes');
|
|
41
|
+
const filterTier = getArg('tier') ? parseInt(getArg('tier'), 10) : null;
|
|
42
|
+
const delay = parseInt(getArg('delay') || '100', 10);
|
|
43
|
+
const TIMEOUT_MS = parseInt(getArg('timeout') || '10000', 10);
|
|
44
|
+
|
|
45
|
+
// ── Site Configuration ──────────────────────────────────────────────
|
|
46
|
+
function getConfiguredSites() {
|
|
47
|
+
const raw = process.env.WP_SITES_CONFIG;
|
|
48
|
+
if (!raw) return [];
|
|
49
|
+
try {
|
|
50
|
+
const sites = JSON.parse(raw);
|
|
51
|
+
return sites.map(s => ({ id: s.id, url: s.url }));
|
|
52
|
+
} catch { return []; }
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Tool Registry ────────────────────────────────────────────────────
|
|
56
|
+
// All 148+ tools classified by module, service, and type.
|
|
57
|
+
const TOOL_REGISTRY = [
|
|
58
|
+
// --- WordPress Core: Content ---
|
|
59
|
+
{ name: 'list_content', module: 'unified-content', service: 'wordpress_core', type: 'read', safeArgs: { content_type: 'post', per_page: 1 } },
|
|
60
|
+
{ name: 'get_content', module: 'unified-content', service: 'wordpress_core', type: 'read', safeArgs: 'dynamic' },
|
|
61
|
+
{ name: 'create_content', module: 'unified-content', service: 'wordpress_core', type: 'write' },
|
|
62
|
+
{ name: 'update_content', module: 'unified-content', service: 'wordpress_core', type: 'write' },
|
|
63
|
+
{ name: 'delete_content', module: 'unified-content', service: 'wordpress_core', type: 'write' },
|
|
64
|
+
{ name: 'discover_content_types', module: 'unified-content', service: 'wordpress_core', type: 'read', safeArgs: {} },
|
|
65
|
+
{ name: 'find_content_by_url', module: 'unified-content', service: 'wordpress_core', type: 'read', safeArgs: 'dynamic' },
|
|
66
|
+
{ name: 'get_content_by_slug', module: 'unified-content', service: 'wordpress_core', type: 'read', safeArgs: 'dynamic' },
|
|
67
|
+
|
|
68
|
+
// --- WordPress Core: Taxonomies ---
|
|
69
|
+
{ name: 'discover_taxonomies', module: 'unified-taxonomies', service: 'wordpress_core', type: 'read', safeArgs: {} },
|
|
70
|
+
{ name: 'list_terms', module: 'unified-taxonomies', service: 'wordpress_core', type: 'read', safeArgs: { taxonomy: 'category', per_page: 1 } },
|
|
71
|
+
{ name: 'get_term', module: 'unified-taxonomies', service: 'wordpress_core', type: 'read', safeArgs: { taxonomy: 'category', id: 1 } },
|
|
72
|
+
{ name: 'create_term', module: 'unified-taxonomies', service: 'wordpress_core', type: 'write' },
|
|
73
|
+
{ name: 'update_term', module: 'unified-taxonomies', service: 'wordpress_core', type: 'write' },
|
|
74
|
+
{ name: 'delete_term', module: 'unified-taxonomies', service: 'wordpress_core', type: 'write' },
|
|
75
|
+
{ name: 'assign_terms_to_content', module: 'unified-taxonomies', service: 'wordpress_core', type: 'write' },
|
|
76
|
+
{ name: 'get_content_terms', module: 'unified-taxonomies', service: 'wordpress_core', type: 'read', safeArgs: 'dynamic' },
|
|
77
|
+
|
|
78
|
+
// --- WordPress Core: Comments ---
|
|
79
|
+
{ name: 'list_comments', module: 'comments', service: 'wordpress_core', type: 'read', safeArgs: { per_page: 1 } },
|
|
80
|
+
{ name: 'get_comment', module: 'comments', service: 'wordpress_core', type: 'read', safeArgs: 'dynamic' },
|
|
81
|
+
{ name: 'create_comment', module: 'comments', service: 'wordpress_core', type: 'write' },
|
|
82
|
+
{ name: 'update_comment', module: 'comments', service: 'wordpress_core', type: 'write' },
|
|
83
|
+
{ name: 'delete_comment', module: 'comments', service: 'wordpress_core', type: 'write' },
|
|
84
|
+
|
|
85
|
+
// --- WordPress Core: Media ---
|
|
86
|
+
{ name: 'list_media', module: 'media', service: 'wordpress_core', type: 'read', safeArgs: { per_page: 1 } },
|
|
87
|
+
{ name: 'get_media', module: 'media', service: 'wordpress_core', type: 'read', safeArgs: 'dynamic' },
|
|
88
|
+
{ name: 'create_media', module: 'media', service: 'wordpress_core', type: 'write' },
|
|
89
|
+
{ name: 'edit_media', module: 'media', service: 'wordpress_core', type: 'write' },
|
|
90
|
+
{ name: 'delete_media', module: 'media', service: 'wordpress_core', type: 'write' },
|
|
91
|
+
|
|
92
|
+
// --- WordPress Core: Users ---
|
|
93
|
+
{ name: 'list_users', module: 'users', service: 'wordpress_core', type: 'read', safeArgs: { per_page: 1 } },
|
|
94
|
+
{ name: 'get_user', module: 'users', service: 'wordpress_core', type: 'read', safeArgs: { id: 1 } },
|
|
95
|
+
{ name: 'get_me', module: 'users', service: 'wordpress_core', type: 'read', safeArgs: {} },
|
|
96
|
+
{ name: 'create_user', module: 'users', service: 'wordpress_core', type: 'write' },
|
|
97
|
+
{ name: 'update_user', module: 'users', service: 'wordpress_core', type: 'write' },
|
|
98
|
+
{ name: 'delete_user', module: 'users', service: 'wordpress_core', type: 'write' },
|
|
99
|
+
|
|
100
|
+
// --- WordPress Core: Plugins ---
|
|
101
|
+
{ name: 'list_plugins', module: 'plugins', service: 'wordpress_core', type: 'read', safeArgs: { status: 'active' } },
|
|
102
|
+
{ name: 'get_plugin', module: 'plugins', service: 'wordpress_core', type: 'read', safeArgs: 'dynamic' },
|
|
103
|
+
{ name: 'activate_plugin', module: 'plugins', service: 'wordpress_core', type: 'write' },
|
|
104
|
+
{ name: 'deactivate_plugin', module: 'plugins', service: 'wordpress_core', type: 'write' },
|
|
105
|
+
{ name: 'create_plugin', module: 'plugins', service: 'wordpress_core', type: 'write' },
|
|
106
|
+
{ name: 'delete_plugin', module: 'plugins', service: 'wordpress_core', type: 'write' },
|
|
107
|
+
|
|
108
|
+
// --- WordPress Core: Search ---
|
|
109
|
+
{ name: 'wp_search', module: 'search', service: 'wordpress_core', type: 'read', safeArgs: { search: 'test', per_page: 1 } },
|
|
110
|
+
|
|
111
|
+
// --- WordPress Core: Plugin Repository ---
|
|
112
|
+
{ name: 'search_plugin_repository', module: 'plugin-repository', service: 'wordpress_core', type: 'read', safeArgs: { search: 'seo' } },
|
|
113
|
+
{ name: 'get_plugin_details', module: 'plugin-repository', service: 'wordpress_core', type: 'read', safeArgs: { slug: 'akismet' } },
|
|
114
|
+
|
|
115
|
+
// --- WordPress Core: Site Management ---
|
|
116
|
+
{ name: 'switch_site', module: 'server', service: 'wordpress_core', type: 'write' },
|
|
117
|
+
{ name: 'list_sites', module: 'server', service: 'wordpress_core', type: 'read', safeArgs: {} },
|
|
118
|
+
{ name: 'get_active_site', module: 'server', service: 'wordpress_core', type: 'read', safeArgs: {} },
|
|
119
|
+
|
|
120
|
+
// --- Multisite: Network ---
|
|
121
|
+
{ name: 'ms_list_network_plugins', module: 'multisite-network', service: 'multisite', type: 'read', safeArgs: {} },
|
|
122
|
+
{ name: 'ms_network_activate_plugin', module: 'multisite-network', service: 'multisite', type: 'write' },
|
|
123
|
+
{ name: 'ms_network_deactivate_plugin', module: 'multisite-network', service: 'multisite', type: 'write' },
|
|
124
|
+
{ name: 'ms_list_super_admins', module: 'multisite-network', service: 'multisite', type: 'read', safeArgs: {} },
|
|
125
|
+
{ name: 'ms_get_network_settings', module: 'multisite-network', service: 'multisite', type: 'read', safeArgs: {} },
|
|
126
|
+
|
|
127
|
+
// --- Multisite: Sites ---
|
|
128
|
+
{ name: 'ms_list_sites', module: 'multisite-sites', service: 'multisite', type: 'read', safeArgs: {} },
|
|
129
|
+
{ name: 'ms_get_site', module: 'multisite-sites', service: 'multisite', type: 'read', safeArgs: { site_id: 1 } },
|
|
130
|
+
{ name: 'ms_create_site', module: 'multisite-sites', service: 'multisite', type: 'write' },
|
|
131
|
+
{ name: 'ms_activate_site', module: 'multisite-sites', service: 'multisite', type: 'write' },
|
|
132
|
+
{ name: 'ms_delete_site', module: 'multisite-sites', service: 'multisite', type: 'write' },
|
|
133
|
+
|
|
134
|
+
// --- WooCommerce: Products ---
|
|
135
|
+
{ name: 'wc_list_products', module: 'wc-products', service: 'woocommerce', type: 'read', safeArgs: { per_page: 1 } },
|
|
136
|
+
{ name: 'wc_get_product', module: 'wc-products', service: 'woocommerce', type: 'read', safeArgs: { id: 1 } },
|
|
137
|
+
{ name: 'wc_create_product', module: 'wc-products', service: 'woocommerce', type: 'write' },
|
|
138
|
+
{ name: 'wc_update_product', module: 'wc-products', service: 'woocommerce', type: 'write' },
|
|
139
|
+
{ name: 'wc_delete_product', module: 'wc-products', service: 'woocommerce', type: 'write' },
|
|
140
|
+
{ name: 'wc_list_product_categories', module: 'wc-products', service: 'woocommerce', type: 'read', safeArgs: { per_page: 1 } },
|
|
141
|
+
{ name: 'wc_list_product_variations', module: 'wc-products', service: 'woocommerce', type: 'read', safeArgs: { product_id: 1 } },
|
|
142
|
+
|
|
143
|
+
// --- WooCommerce: Orders ---
|
|
144
|
+
{ name: 'wc_list_orders', module: 'wc-orders', service: 'woocommerce', type: 'read', safeArgs: { per_page: 1 } },
|
|
145
|
+
{ name: 'wc_get_order', module: 'wc-orders', service: 'woocommerce', type: 'read', safeArgs: { id: 1 } },
|
|
146
|
+
{ name: 'wc_update_order_status', module: 'wc-orders', service: 'woocommerce', type: 'write' },
|
|
147
|
+
{ name: 'wc_list_order_notes', module: 'wc-orders', service: 'woocommerce', type: 'read', safeArgs: { order_id: 1 } },
|
|
148
|
+
{ name: 'wc_create_order_note', module: 'wc-orders', service: 'woocommerce', type: 'write' },
|
|
149
|
+
{ name: 'wc_create_refund', module: 'wc-orders', service: 'woocommerce', type: 'write' },
|
|
150
|
+
|
|
151
|
+
// --- WooCommerce: Customers ---
|
|
152
|
+
{ name: 'wc_list_customers', module: 'wc-customers', service: 'woocommerce', type: 'read', safeArgs: { per_page: 1 } },
|
|
153
|
+
{ name: 'wc_get_customer', module: 'wc-customers', service: 'woocommerce', type: 'read', safeArgs: { id: 1 } },
|
|
154
|
+
{ name: 'wc_create_customer', module: 'wc-customers', service: 'woocommerce', type: 'write' },
|
|
155
|
+
{ name: 'wc_update_customer', module: 'wc-customers', service: 'woocommerce', type: 'write' },
|
|
156
|
+
|
|
157
|
+
// --- WooCommerce: Coupons ---
|
|
158
|
+
{ name: 'wc_list_coupons', module: 'wc-coupons', service: 'woocommerce', type: 'read', safeArgs: { per_page: 1 } },
|
|
159
|
+
{ name: 'wc_get_coupon', module: 'wc-coupons', service: 'woocommerce', type: 'read', safeArgs: { id: 1 } },
|
|
160
|
+
{ name: 'wc_create_coupon', module: 'wc-coupons', service: 'woocommerce', type: 'write' },
|
|
161
|
+
{ name: 'wc_delete_coupon', module: 'wc-coupons', service: 'woocommerce', type: 'write' },
|
|
162
|
+
|
|
163
|
+
// --- WooCommerce: Reports ---
|
|
164
|
+
{ name: 'wc_get_sales_report', module: 'wc-reports', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
165
|
+
{ name: 'wc_get_top_sellers', module: 'wc-reports', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
166
|
+
{ name: 'wc_get_orders_totals', module: 'wc-reports', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
167
|
+
{ name: 'wc_get_products_totals', module: 'wc-reports', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
168
|
+
{ name: 'wc_get_customers_totals', module: 'wc-reports', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
169
|
+
|
|
170
|
+
// --- WooCommerce: Settings ---
|
|
171
|
+
{ name: 'wc_list_payment_gateways', module: 'wc-settings', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
172
|
+
{ name: 'wc_list_shipping_zones', module: 'wc-settings', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
173
|
+
{ name: 'wc_get_tax_classes', module: 'wc-settings', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
174
|
+
{ name: 'wc_get_system_status', module: 'wc-settings', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
175
|
+
|
|
176
|
+
// --- WooCommerce: Webhooks ---
|
|
177
|
+
{ name: 'wc_list_webhooks', module: 'wc-webhooks', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
178
|
+
{ name: 'wc_create_webhook', module: 'wc-webhooks', service: 'woocommerce', type: 'write' },
|
|
179
|
+
{ name: 'wc_update_webhook', module: 'wc-webhooks', service: 'woocommerce', type: 'write' },
|
|
180
|
+
{ name: 'wc_delete_webhook', module: 'wc-webhooks', service: 'woocommerce', type: 'write' },
|
|
181
|
+
|
|
182
|
+
// --- WooCommerce: Workflows ---
|
|
183
|
+
{ name: 'wf_list_triggers', module: 'wc-workflows', service: 'woocommerce', type: 'read', safeArgs: {} },
|
|
184
|
+
{ name: 'wf_create_trigger', module: 'wc-workflows', service: 'woocommerce', type: 'write' },
|
|
185
|
+
{ name: 'wf_update_trigger', module: 'wc-workflows', service: 'woocommerce', type: 'write' },
|
|
186
|
+
{ name: 'wf_delete_trigger', module: 'wc-workflows', service: 'woocommerce', type: 'write' },
|
|
187
|
+
|
|
188
|
+
// --- Mailchimp ---
|
|
189
|
+
{ name: 'mc_list_audiences', module: 'mailchimp', service: 'mailchimp', type: 'read', safeArgs: {} },
|
|
190
|
+
{ name: 'mc_get_audience_members', module: 'mailchimp', service: 'mailchimp', type: 'read', safeArgs: { list_id: 'default' } },
|
|
191
|
+
{ name: 'mc_create_campaign', module: 'mailchimp', service: 'mailchimp', type: 'write' },
|
|
192
|
+
{ name: 'mc_update_campaign_content', module: 'mailchimp', service: 'mailchimp', type: 'write' },
|
|
193
|
+
{ name: 'mc_send_campaign', module: 'mailchimp', service: 'mailchimp', type: 'write' },
|
|
194
|
+
{ name: 'mc_get_campaign_report', module: 'mailchimp', service: 'mailchimp', type: 'read', safeArgs: { campaign_id: 'test' } },
|
|
195
|
+
{ name: 'mc_add_subscriber', module: 'mailchimp', service: 'mailchimp', type: 'write' },
|
|
196
|
+
|
|
197
|
+
// --- Buffer ---
|
|
198
|
+
{ name: 'buf_list_profiles', module: 'buffer', service: 'buffer', type: 'read', safeArgs: {} },
|
|
199
|
+
{ name: 'buf_create_update', module: 'buffer', service: 'buffer', type: 'write' },
|
|
200
|
+
{ name: 'buf_list_pending', module: 'buffer', service: 'buffer', type: 'read', safeArgs: { profile_id: 'default' } },
|
|
201
|
+
{ name: 'buf_list_sent', module: 'buffer', service: 'buffer', type: 'read', safeArgs: { profile_id: 'default' } },
|
|
202
|
+
{ name: 'buf_get_analytics', module: 'buffer', service: 'buffer', type: 'read', safeArgs: { profile_id: 'default' } },
|
|
203
|
+
|
|
204
|
+
// --- SendGrid ---
|
|
205
|
+
{ name: 'sg_send_email', module: 'sendgrid', service: 'sendgrid', type: 'write' },
|
|
206
|
+
{ name: 'sg_list_templates', module: 'sendgrid', service: 'sendgrid', type: 'read', safeArgs: {} },
|
|
207
|
+
{ name: 'sg_get_template', module: 'sendgrid', service: 'sendgrid', type: 'read', safeArgs: { template_id: 'test' } },
|
|
208
|
+
{ name: 'sg_list_contacts', module: 'sendgrid', service: 'sendgrid', type: 'read', safeArgs: {} },
|
|
209
|
+
{ name: 'sg_add_contacts', module: 'sendgrid', service: 'sendgrid', type: 'write' },
|
|
210
|
+
{ name: 'sg_get_stats', module: 'sendgrid', service: 'sendgrid', type: 'read', safeArgs: {} },
|
|
211
|
+
|
|
212
|
+
// --- Google Search Console ---
|
|
213
|
+
{ name: 'gsc_list_sites', module: 'gsc', service: 'gsc', type: 'read', safeArgs: {} },
|
|
214
|
+
{ name: 'gsc_search_analytics', module: 'gsc', service: 'gsc', type: 'read', safeArgs: { start_date: '2025-01-01', end_date: '2025-01-31', dimensions: ['query'], row_limit: 5 } },
|
|
215
|
+
{ name: 'gsc_inspect_url', module: 'gsc', service: 'gsc', type: 'read', safeArgs: { url: '/' } },
|
|
216
|
+
{ name: 'gsc_list_sitemaps', module: 'gsc', service: 'gsc', type: 'read', safeArgs: {} },
|
|
217
|
+
{ name: 'gsc_submit_sitemap', module: 'gsc', service: 'gsc', type: 'write' },
|
|
218
|
+
{ name: 'gsc_delete_sitemap', module: 'gsc', service: 'gsc', type: 'write' },
|
|
219
|
+
{ name: 'gsc_top_queries', module: 'gsc', service: 'gsc', type: 'read', safeArgs: { days: 7, limit: 5 } },
|
|
220
|
+
{ name: 'gsc_page_performance', module: 'gsc', service: 'gsc', type: 'read', safeArgs: { days: 7, limit: 5 } },
|
|
221
|
+
|
|
222
|
+
// --- Google Analytics 4 ---
|
|
223
|
+
{ name: 'ga4_run_report', module: 'ga4', service: 'ga4', type: 'read', safeArgs: { dimensions: ['date'], metrics: ['activeUsers'], start_date: '7daysAgo', end_date: 'today' } },
|
|
224
|
+
{ name: 'ga4_get_realtime', module: 'ga4', service: 'ga4', type: 'read', safeArgs: {} },
|
|
225
|
+
{ name: 'ga4_top_pages', module: 'ga4', service: 'ga4', type: 'read', safeArgs: { days: 7, limit: 5 } },
|
|
226
|
+
{ name: 'ga4_traffic_sources', module: 'ga4', service: 'ga4', type: 'read', safeArgs: { days: 7, limit: 5 } },
|
|
227
|
+
{ name: 'ga4_user_demographics', module: 'ga4', service: 'ga4', type: 'read', safeArgs: { days: 7 } },
|
|
228
|
+
{ name: 'ga4_conversion_events', module: 'ga4', service: 'ga4', type: 'read', safeArgs: { days: 7, limit: 5 } },
|
|
229
|
+
|
|
230
|
+
// --- Plausible ---
|
|
231
|
+
{ name: 'pl_get_stats', module: 'plausible', service: 'plausible', type: 'read', safeArgs: { period: '7d' } },
|
|
232
|
+
{ name: 'pl_get_timeseries', module: 'plausible', service: 'plausible', type: 'read', safeArgs: { period: '7d' } },
|
|
233
|
+
{ name: 'pl_get_breakdown', module: 'plausible', service: 'plausible', type: 'read', safeArgs: { property: 'visit:source', period: '7d' } },
|
|
234
|
+
{ name: 'pl_get_realtime', module: 'plausible', service: 'plausible', type: 'read', safeArgs: {} },
|
|
235
|
+
|
|
236
|
+
// --- Core Web Vitals ---
|
|
237
|
+
{ name: 'cwv_analyze_url', module: 'cwv', service: 'cwv', type: 'read', safeArgs: { url: 'https://example.com' } },
|
|
238
|
+
{ name: 'cwv_batch_analyze', module: 'cwv', service: 'cwv', type: 'read', safeArgs: { urls: ['https://example.com'] } },
|
|
239
|
+
{ name: 'cwv_get_field_data', module: 'cwv', service: 'cwv', type: 'read', safeArgs: { url: 'https://example.com' } },
|
|
240
|
+
{ name: 'cwv_compare_pages', module: 'cwv', service: 'cwv', type: 'read', safeArgs: { urls: ['https://example.com', 'https://example.org'] } },
|
|
241
|
+
|
|
242
|
+
// --- Slack ---
|
|
243
|
+
{ name: 'slack_send_alert', module: 'slack', service: 'slack', type: 'write' },
|
|
244
|
+
{ name: 'slack_send_message', module: 'slack', service: 'slack', type: 'write' },
|
|
245
|
+
{ name: 'slack_list_channels', module: 'slack', service: 'slack', type: 'read', safeArgs: {} },
|
|
246
|
+
|
|
247
|
+
// --- LinkedIn ---
|
|
248
|
+
{ name: 'li_get_profile', module: 'linkedin', service: 'linkedin', type: 'read', safeArgs: {} },
|
|
249
|
+
{ name: 'li_create_post', module: 'linkedin', service: 'linkedin', type: 'write' },
|
|
250
|
+
{ name: 'li_create_article', module: 'linkedin', service: 'linkedin', type: 'write' },
|
|
251
|
+
{ name: 'li_get_analytics', module: 'linkedin', service: 'linkedin', type: 'read', safeArgs: {} },
|
|
252
|
+
{ name: 'li_list_posts', module: 'linkedin', service: 'linkedin', type: 'read', safeArgs: {} },
|
|
253
|
+
|
|
254
|
+
// --- Twitter ---
|
|
255
|
+
{ name: 'tw_create_tweet', module: 'twitter', service: 'twitter', type: 'write' },
|
|
256
|
+
{ name: 'tw_create_thread', module: 'twitter', service: 'twitter', type: 'write' },
|
|
257
|
+
{ name: 'tw_get_metrics', module: 'twitter', service: 'twitter', type: 'read', safeArgs: {} },
|
|
258
|
+
{ name: 'tw_list_tweets', module: 'twitter', service: 'twitter', type: 'read', safeArgs: {} },
|
|
259
|
+
{ name: 'tw_delete_tweet', module: 'twitter', service: 'twitter', type: 'write' },
|
|
260
|
+
|
|
261
|
+
// --- Schema.org Structured Data ---
|
|
262
|
+
{ name: 'sd_validate', module: 'schema', service: 'wordpress_core', type: 'read', safeArgs: { url: '/' } },
|
|
263
|
+
{ name: 'sd_inject', module: 'schema', service: 'wordpress_core', type: 'write' },
|
|
264
|
+
{ name: 'sd_list_schemas', module: 'schema', service: 'wordpress_core', type: 'read', safeArgs: {} },
|
|
265
|
+
];
|
|
266
|
+
|
|
267
|
+
// Service detection probes: one canonical tool per service
|
|
268
|
+
const SERVICE_PROBES = {
|
|
269
|
+
wordpress_core: 'get_active_site',
|
|
270
|
+
woocommerce: 'wc_list_products',
|
|
271
|
+
multisite: 'ms_list_sites',
|
|
272
|
+
mailchimp: 'mc_list_audiences',
|
|
273
|
+
buffer: 'buf_list_profiles',
|
|
274
|
+
sendgrid: 'sg_list_templates',
|
|
275
|
+
gsc: 'gsc_list_sites',
|
|
276
|
+
ga4: 'ga4_top_pages',
|
|
277
|
+
plausible: 'pl_get_stats',
|
|
278
|
+
cwv: 'cwv_analyze_url',
|
|
279
|
+
slack: 'slack_list_channels',
|
|
280
|
+
linkedin: 'li_get_profile',
|
|
281
|
+
twitter: 'tw_list_tweets',
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
// ── Write Sequences ─────────────────────────────────────────────────
|
|
285
|
+
// CRUD sequences: create → verify → update → verify → delete → verify(404)
|
|
286
|
+
// Each step extracts context for subsequent steps via `extract` and `as`.
|
|
287
|
+
const WRITE_SEQUENCES = [
|
|
288
|
+
// --- Tier 1: Safe standalone CRUD ---
|
|
289
|
+
{
|
|
290
|
+
name: 'content', tier: 1, service: 'wordpress_core',
|
|
291
|
+
steps: [
|
|
292
|
+
{ action: 'create', tool: 'create_content', args: { content_type: 'post', title: '[TEST] Validation Post', content: 'Test content — created by validation runner', status: 'draft' }, extract: 'id' },
|
|
293
|
+
{ action: 'verify', tool: 'get_content', argsFrom: ctx => ({ content_type: 'post', id: ctx.id }), expect: 'exists' },
|
|
294
|
+
{ action: 'update', tool: 'update_content', argsFrom: ctx => ({ content_type: 'post', id: ctx.id, title: '[TEST] Updated Validation Post' }) },
|
|
295
|
+
{ action: 'verify', tool: 'get_content', argsFrom: ctx => ({ content_type: 'post', id: ctx.id }), expect: 'exists' },
|
|
296
|
+
{ action: 'delete', tool: 'delete_content', argsFrom: ctx => ({ content_type: 'post', id: ctx.id, force: true }) },
|
|
297
|
+
],
|
|
298
|
+
cleanup: ctx => ctx.id ? { tool: 'delete_content', args: { content_type: 'post', id: ctx.id, force: true } } : null,
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: 'term', tier: 1, service: 'wordpress_core',
|
|
302
|
+
steps: [
|
|
303
|
+
{ action: 'create', tool: 'create_term', args: { taxonomy: 'category', name: '[TEST] Validation Category' }, extract: 'id' },
|
|
304
|
+
{ action: 'verify', tool: 'get_term', argsFrom: ctx => ({ taxonomy: 'category', id: ctx.id }), expect: 'exists' },
|
|
305
|
+
{ action: 'update', tool: 'update_term', argsFrom: ctx => ({ taxonomy: 'category', id: ctx.id, name: '[TEST] Updated Category' }) },
|
|
306
|
+
{ action: 'verify', tool: 'get_term', argsFrom: ctx => ({ taxonomy: 'category', id: ctx.id }), expect: 'exists' },
|
|
307
|
+
{ action: 'delete', tool: 'delete_term', argsFrom: ctx => ({ taxonomy: 'category', id: ctx.id, force: true }) },
|
|
308
|
+
],
|
|
309
|
+
cleanup: ctx => ctx.id ? { tool: 'delete_term', args: { taxonomy: 'category', id: ctx.id, force: true } } : null,
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
name: 'comment', tier: 1, service: 'wordpress_core',
|
|
313
|
+
// Needs a real post — uses dynamic postId resolved before running
|
|
314
|
+
steps: [
|
|
315
|
+
{ action: 'create', tool: 'create_comment', argsFrom: ctx => ({ post: ctx._postId, content: '[TEST] Validation comment', author_name: 'Validator', author_email: 'test@validation.local' }), extract: 'id' },
|
|
316
|
+
{ action: 'verify', tool: 'get_comment', argsFrom: ctx => ({ id: ctx.id }), expect: 'exists' },
|
|
317
|
+
{ action: 'update', tool: 'update_comment', argsFrom: ctx => ({ id: ctx.id, content: '[TEST] Updated comment' }) },
|
|
318
|
+
{ action: 'verify', tool: 'get_comment', argsFrom: ctx => ({ id: ctx.id }), expect: 'exists' },
|
|
319
|
+
{ action: 'delete', tool: 'delete_comment', argsFrom: ctx => ({ id: ctx.id, force: true }) },
|
|
320
|
+
],
|
|
321
|
+
cleanup: ctx => ctx.id ? { tool: 'delete_comment', args: { id: ctx.id, force: true } } : null,
|
|
322
|
+
},
|
|
323
|
+
{
|
|
324
|
+
name: 'user', tier: 1, service: 'wordpress_core',
|
|
325
|
+
steps: [
|
|
326
|
+
{ action: 'create', tool: 'create_user', argsFrom: () => ({ username: 'test_validator_' + Date.now(), email: `validator_${Date.now()}@test.local`, password: 'Val1dation!Test#2026', role: 'subscriber' }), extract: 'id' },
|
|
327
|
+
{ action: 'verify', tool: 'get_user', argsFrom: ctx => ({ id: ctx.id }), expect: 'exists' },
|
|
328
|
+
{ action: 'update', tool: 'update_user', argsFrom: ctx => ({ id: ctx.id, first_name: 'Test', last_name: 'Validator' }) },
|
|
329
|
+
{ action: 'verify', tool: 'get_user', argsFrom: ctx => ({ id: ctx.id }), expect: 'exists' },
|
|
330
|
+
{ action: 'delete', tool: 'delete_user', argsFrom: ctx => ({ id: ctx.id, reassign: 1, force: true }) },
|
|
331
|
+
],
|
|
332
|
+
cleanup: ctx => ctx.id ? { tool: 'delete_user', args: { id: ctx.id, reassign: 1, force: true } } : null,
|
|
333
|
+
},
|
|
334
|
+
|
|
335
|
+
// --- Tier 2: CRUD with dependencies ---
|
|
336
|
+
{
|
|
337
|
+
name: 'media', tier: 2, service: 'wordpress_core',
|
|
338
|
+
steps: [
|
|
339
|
+
{ action: 'create', tool: 'create_media', args: { title: '[TEST] Validation Image', source_url: 'https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png' }, extract: 'id' },
|
|
340
|
+
{ action: 'verify', tool: 'get_media', argsFrom: ctx => ({ id: ctx.id }), expect: 'exists' },
|
|
341
|
+
{ action: 'update', tool: 'edit_media', argsFrom: ctx => ({ id: ctx.id, title: '[TEST] Updated Image', alt_text: 'validation test' }) },
|
|
342
|
+
{ action: 'verify', tool: 'get_media', argsFrom: ctx => ({ id: ctx.id }), expect: 'exists' },
|
|
343
|
+
{ action: 'delete', tool: 'delete_media', argsFrom: ctx => ({ id: ctx.id, force: true }) },
|
|
344
|
+
],
|
|
345
|
+
cleanup: ctx => ctx.id ? { tool: 'delete_media', args: { id: ctx.id, force: true } } : null,
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
name: 'plugin', tier: 2, service: 'wordpress_core',
|
|
349
|
+
steps: [
|
|
350
|
+
{ action: 'create', tool: 'create_plugin', args: { slug: 'hello-dolly', status: 'inactive' }, extract: 'plugin' },
|
|
351
|
+
{ action: 'verify', tool: 'get_plugin', argsFrom: ctx => ({ plugin: ctx.plugin }), expect: 'exists' },
|
|
352
|
+
{ action: 'update', tool: 'activate_plugin', argsFrom: ctx => ({ plugin: ctx.plugin }) },
|
|
353
|
+
{ action: 'update', tool: 'deactivate_plugin', argsFrom: ctx => ({ plugin: ctx.plugin }) },
|
|
354
|
+
{ action: 'delete', tool: 'delete_plugin', argsFrom: ctx => ({ plugin: ctx.plugin }) },
|
|
355
|
+
],
|
|
356
|
+
cleanup: ctx => ctx.plugin ? { tool: 'delete_plugin', args: { plugin: ctx.plugin } } : null,
|
|
357
|
+
},
|
|
358
|
+
{
|
|
359
|
+
name: 'assign_terms', tier: 2, service: 'wordpress_core',
|
|
360
|
+
steps: [
|
|
361
|
+
// Create own test entities
|
|
362
|
+
{ action: 'create', tool: 'create_content', args: { content_type: 'post', title: '[TEST] Terms Assignment Post', content: 'test', status: 'draft' }, extract: 'id', as: 'postId' },
|
|
363
|
+
{ action: 'create', tool: 'create_term', args: { taxonomy: 'post_tag', name: '[TEST] Validation Tag' }, extract: 'id', as: 'tagId' },
|
|
364
|
+
{ action: 'update', tool: 'assign_terms_to_content', argsFrom: ctx => ({ content_id: ctx.postId, content_type: 'post', taxonomy: 'post_tag', terms: [ctx.tagId] }) },
|
|
365
|
+
{ action: 'verify', tool: 'get_content_terms', argsFrom: ctx => ({ content_id: ctx.postId, content_type: 'post' }), expect: 'exists' },
|
|
366
|
+
// Cleanup created entities
|
|
367
|
+
{ action: 'delete', tool: 'delete_content', argsFrom: ctx => ({ content_type: 'post', id: ctx.postId, force: true }) },
|
|
368
|
+
{ action: 'delete', tool: 'delete_term', argsFrom: ctx => ({ taxonomy: 'post_tag', id: ctx.tagId, force: true }) },
|
|
369
|
+
],
|
|
370
|
+
cleanup: ctx => {
|
|
371
|
+
const tasks = [];
|
|
372
|
+
if (ctx.postId) tasks.push({ tool: 'delete_content', args: { content_type: 'post', id: ctx.postId, force: true } });
|
|
373
|
+
if (ctx.tagId) tasks.push({ tool: 'delete_term', args: { taxonomy: 'post_tag', id: ctx.tagId, force: true } });
|
|
374
|
+
return tasks.length ? tasks : null;
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
// --- Tier 3: Special operations ---
|
|
379
|
+
{
|
|
380
|
+
name: 'switch_site', tier: 3, service: 'wordpress_core',
|
|
381
|
+
steps: [
|
|
382
|
+
{ action: 'verify', tool: 'get_active_site', args: {}, extract: 'site_id', as: 'originalSite', extractFn: text => { try { const d = JSON.parse(text); return d.site_id || d.id || text; } catch { return text; } } },
|
|
383
|
+
{ action: 'update', tool: 'switch_site', args: { site_id: 'bioinagro' } },
|
|
384
|
+
{ action: 'verify', tool: 'get_active_site', args: {}, expect: 'exists' },
|
|
385
|
+
{ action: 'update', tool: 'switch_site', argsFrom: ctx => ({ site_id: ctx.originalSite }) },
|
|
386
|
+
],
|
|
387
|
+
cleanup: null,
|
|
388
|
+
},
|
|
389
|
+
];
|
|
390
|
+
|
|
391
|
+
// ── Helpers ──────────────────────────────────────────────────────────
|
|
392
|
+
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
393
|
+
|
|
394
|
+
function truncate(str, max = 200) {
|
|
395
|
+
if (!str) return '';
|
|
396
|
+
const s = String(str).replace(/\n/g, ' ');
|
|
397
|
+
return s.length > max ? s.slice(0, max) + '...' : s;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function log(msg) {
|
|
401
|
+
process.stderr.write(`[validation] ${msg}\n`);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// ── Write Sequence Executor ──────────────────────────────────────────
|
|
405
|
+
async function executeWriteSequences(client, services, delay, tierFilter = null) {
|
|
406
|
+
const results = []; // per-tool results
|
|
407
|
+
const cleanupQueue = []; // { tool, args } for emergency cleanup
|
|
408
|
+
|
|
409
|
+
// Resolve a dynamic postId for comment sequence
|
|
410
|
+
let sharedPostId = null;
|
|
411
|
+
if (services.wordpress_core?.configured) {
|
|
412
|
+
try {
|
|
413
|
+
const r = await client.callTool({ name: 'list_content', arguments: { content_type: 'post', per_page: 1 } });
|
|
414
|
+
const text = r.content?.[0]?.text || '';
|
|
415
|
+
sharedPostId = JSON.parse(text)?.[0]?.id || null;
|
|
416
|
+
} catch { /* ignore */ }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Filter sequences by tier and service
|
|
420
|
+
const sequences = WRITE_SEQUENCES.filter(seq => {
|
|
421
|
+
if (tierFilter && seq.tier !== tierFilter) return false;
|
|
422
|
+
const svc = services[seq.service];
|
|
423
|
+
if (!svc || !svc.configured) return false;
|
|
424
|
+
return true;
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
log(`Write sequences to run: ${sequences.map(s => `${s.name}(T${s.tier})`).join(', ') || 'none'}`);
|
|
428
|
+
|
|
429
|
+
for (const seq of sequences) {
|
|
430
|
+
log(`\n ── Sequence: ${seq.name} (Tier ${seq.tier}) ──`);
|
|
431
|
+
const ctx = { _postId: sharedPostId }; // shared context for this sequence
|
|
432
|
+
let aborted = false;
|
|
433
|
+
|
|
434
|
+
for (const step of seq.steps) {
|
|
435
|
+
if (aborted && step.action !== 'delete') {
|
|
436
|
+
// After abort, only attempt deletes (cleanup)
|
|
437
|
+
results.push({
|
|
438
|
+
name: step.tool,
|
|
439
|
+
type: 'write',
|
|
440
|
+
status: 'skipped',
|
|
441
|
+
tested_at: null,
|
|
442
|
+
duration_ms: null,
|
|
443
|
+
response_preview: null,
|
|
444
|
+
error_message: null,
|
|
445
|
+
skip_reason: `Sequence "${seq.name}" aborted at earlier step`,
|
|
446
|
+
});
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Resolve arguments
|
|
451
|
+
let toolArgs;
|
|
452
|
+
if (step.argsFrom) {
|
|
453
|
+
try {
|
|
454
|
+
toolArgs = step.argsFrom(ctx);
|
|
455
|
+
} catch (err) {
|
|
456
|
+
results.push({
|
|
457
|
+
name: step.tool,
|
|
458
|
+
type: step.action === 'verify' ? 'read' : 'write',
|
|
459
|
+
status: 'error',
|
|
460
|
+
tested_at: new Date().toISOString(),
|
|
461
|
+
duration_ms: 0,
|
|
462
|
+
response_preview: null,
|
|
463
|
+
error_message: `argsFrom failed: ${err.message}`,
|
|
464
|
+
skip_reason: null,
|
|
465
|
+
});
|
|
466
|
+
if (step.action === 'create') aborted = true;
|
|
467
|
+
continue;
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
toolArgs = step.args || {};
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// Execute tool call
|
|
474
|
+
const startTime = Date.now();
|
|
475
|
+
const toolResult = {
|
|
476
|
+
name: step.tool,
|
|
477
|
+
type: step.action === 'verify' ? 'read' : 'write',
|
|
478
|
+
status: 'untested',
|
|
479
|
+
tested_at: null,
|
|
480
|
+
duration_ms: null,
|
|
481
|
+
response_preview: null,
|
|
482
|
+
error_message: null,
|
|
483
|
+
skip_reason: null,
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const result = await Promise.race([
|
|
488
|
+
client.callTool({ name: step.tool, arguments: toolArgs }),
|
|
489
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), TIMEOUT_MS)),
|
|
490
|
+
]);
|
|
491
|
+
const elapsed = Date.now() - startTime;
|
|
492
|
+
const text = result.content?.map(c => c.text || '').join(' ') || '';
|
|
493
|
+
|
|
494
|
+
toolResult.tested_at = new Date().toISOString();
|
|
495
|
+
toolResult.duration_ms = elapsed;
|
|
496
|
+
toolResult.response_preview = truncate(`Seq:${seq.name} ${step.action} → ${text}`, 200);
|
|
497
|
+
|
|
498
|
+
if (result.isError) {
|
|
499
|
+
toolResult.status = 'failed';
|
|
500
|
+
toolResult.error_message = truncate(text);
|
|
501
|
+
if (step.action === 'create') aborted = true;
|
|
502
|
+
} else {
|
|
503
|
+
toolResult.status = 'passed';
|
|
504
|
+
|
|
505
|
+
// Extract values into context
|
|
506
|
+
if (step.extract) {
|
|
507
|
+
let extracted;
|
|
508
|
+
if (step.extractFn) {
|
|
509
|
+
extracted = step.extractFn(text);
|
|
510
|
+
} else {
|
|
511
|
+
try {
|
|
512
|
+
const parsed = JSON.parse(text);
|
|
513
|
+
extracted = parsed[step.extract];
|
|
514
|
+
} catch {
|
|
515
|
+
// Try regex fallback for "id": 123 pattern
|
|
516
|
+
const m = text.match(new RegExp(`"${step.extract}"\\s*:\\s*(\\d+|"[^"]+")`));
|
|
517
|
+
if (m) extracted = m[1].replace(/"/g, '');
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
if (extracted) {
|
|
521
|
+
const key = step.as || step.extract;
|
|
522
|
+
ctx[key] = extracted;
|
|
523
|
+
log(` → extracted ${key}=${extracted}`);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// Register for cleanup if this was a create
|
|
528
|
+
if (step.action === 'create' && seq.cleanup) {
|
|
529
|
+
const cleanupSpec = seq.cleanup(ctx);
|
|
530
|
+
if (cleanupSpec) {
|
|
531
|
+
const specs = Array.isArray(cleanupSpec) ? cleanupSpec : [cleanupSpec];
|
|
532
|
+
for (const spec of specs) {
|
|
533
|
+
const tagged = { ...spec, seq: seq.name };
|
|
534
|
+
// Deduplicate: don't re-add if same tool+args already queued
|
|
535
|
+
const dup = cleanupQueue.find(c => c.tool === tagged.tool && c.seq === tagged.seq && JSON.stringify(c.args) === JSON.stringify(tagged.args));
|
|
536
|
+
if (!dup) cleanupQueue.push(tagged);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
} catch (err) {
|
|
542
|
+
toolResult.tested_at = new Date().toISOString();
|
|
543
|
+
toolResult.duration_ms = Date.now() - startTime;
|
|
544
|
+
toolResult.status = 'error';
|
|
545
|
+
toolResult.error_message = truncate(err.message);
|
|
546
|
+
if (step.action === 'create') aborted = true;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const icon = { passed: '+', failed: 'X', error: '!', skipped: '-' }[toolResult.status] || '?';
|
|
550
|
+
log(` [${icon}] ${step.action}:${step.tool} (${toolResult.duration_ms}ms)`);
|
|
551
|
+
|
|
552
|
+
// For verify steps, don't add duplicate results — they test read tools
|
|
553
|
+
// Only add results for write tools (create/update/delete)
|
|
554
|
+
if (step.action !== 'verify') {
|
|
555
|
+
results.push(toolResult);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// Remove from cleanup queue if delete succeeded
|
|
559
|
+
if (step.action === 'delete' && toolResult.status === 'passed') {
|
|
560
|
+
const idx = cleanupQueue.findIndex(c => c.tool === step.tool && c.seq === seq.name);
|
|
561
|
+
if (idx >= 0) cleanupQueue.splice(idx, 1);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
await sleep(delay);
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// ── Emergency Cleanup Phase ─────────────────────────────────────
|
|
569
|
+
if (cleanupQueue.length > 0) {
|
|
570
|
+
log(`\n ── Cleanup: ${cleanupQueue.length} entity(s) to remove ──`);
|
|
571
|
+
for (const item of cleanupQueue) {
|
|
572
|
+
try {
|
|
573
|
+
log(` Cleaning: ${item.tool} ${JSON.stringify(item.args)}`);
|
|
574
|
+
await client.callTool({ name: item.tool, arguments: item.args });
|
|
575
|
+
log(` → cleaned OK`);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
log(` → CLEANUP FAILED: ${err.message}`);
|
|
578
|
+
log(` ⚠ Manual cleanup needed: ${item.tool} ${JSON.stringify(item.args)}`);
|
|
579
|
+
}
|
|
580
|
+
await sleep(delay);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return results;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// ── Interactive Mode ─────────────────────────────────────────────────
|
|
588
|
+
async function interactiveMode() {
|
|
589
|
+
// Dynamic import of @clack/prompts from server's node_modules
|
|
590
|
+
const clackPath = resolve(SERVER_DIR, 'node_modules/@clack/prompts/dist/index.mjs');
|
|
591
|
+
const p = await import(clackPath);
|
|
592
|
+
|
|
593
|
+
p.intro('WP REST Bridge — Validation Runner v' + RUNNER_VERSION);
|
|
594
|
+
|
|
595
|
+
// 1. Site selection
|
|
596
|
+
const sites = getConfiguredSites();
|
|
597
|
+
let site = null;
|
|
598
|
+
if (sites.length > 0) {
|
|
599
|
+
site = await p.select({
|
|
600
|
+
message: 'Seleziona il sito target',
|
|
601
|
+
options: sites.map(s => ({ value: s.id, label: `${s.id} (${s.url})` })),
|
|
602
|
+
});
|
|
603
|
+
if (p.isCancel(site)) { p.cancel('Operazione annullata.'); return null; }
|
|
604
|
+
} else {
|
|
605
|
+
p.log.warn('Nessun sito configurato in WP_SITES_CONFIG — uso sito di default.');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// 2. Validation type
|
|
609
|
+
const validationType = await p.select({
|
|
610
|
+
message: 'Cosa vuoi validare?',
|
|
611
|
+
options: [
|
|
612
|
+
{ value: 'read', label: 'Solo read tool' },
|
|
613
|
+
{ value: 'write-t1', label: 'Solo write tool (Tier 1 — CRUD base)' },
|
|
614
|
+
{ value: 'write-t2', label: 'Solo write tool (Tier 2 — con dipendenze)' },
|
|
615
|
+
{ value: 'write-all', label: 'Solo write tool (tutti i tier)' },
|
|
616
|
+
{ value: 'all', label: 'Tutto (read + write)' },
|
|
617
|
+
],
|
|
618
|
+
});
|
|
619
|
+
if (p.isCancel(validationType)) { p.cancel('Operazione annullata.'); return null; }
|
|
620
|
+
|
|
621
|
+
// 3. Module filter
|
|
622
|
+
const uniqueModules = [...new Set(TOOL_REGISTRY.map(t => t.module))].sort();
|
|
623
|
+
const filterMod = await p.select({
|
|
624
|
+
message: 'Filtrare per modulo? (opzionale)',
|
|
625
|
+
options: [
|
|
626
|
+
{ value: null, label: 'Tutti i moduli' },
|
|
627
|
+
...uniqueModules.map(m => ({ value: m, label: m })),
|
|
628
|
+
],
|
|
629
|
+
});
|
|
630
|
+
if (p.isCancel(filterMod)) { p.cancel('Operazione annullata.'); return null; }
|
|
631
|
+
|
|
632
|
+
// Derive config from selections
|
|
633
|
+
const testReads = validationType === 'read' || validationType === 'all';
|
|
634
|
+
const doTestWrites = validationType.startsWith('write') || validationType === 'all';
|
|
635
|
+
let tier = null;
|
|
636
|
+
if (validationType === 'write-t1') tier = 1;
|
|
637
|
+
else if (validationType === 'write-t2') tier = 2;
|
|
638
|
+
|
|
639
|
+
// 4. Confirmation
|
|
640
|
+
const typeLabel = {
|
|
641
|
+
'read': 'Read tools',
|
|
642
|
+
'write-t1': 'Write Tier 1',
|
|
643
|
+
'write-t2': 'Write Tier 2',
|
|
644
|
+
'write-all': 'Write tutti i tier',
|
|
645
|
+
'all': 'Read + Write',
|
|
646
|
+
}[validationType];
|
|
647
|
+
const modLabel = filterMod || 'tutti';
|
|
648
|
+
const siteLabel = site || 'default';
|
|
649
|
+
|
|
650
|
+
const confirmed = await p.confirm({
|
|
651
|
+
message: `Eseguire validazione su ${siteLabel}?\n ${typeLabel} | Modulo: ${modLabel}`,
|
|
652
|
+
});
|
|
653
|
+
if (p.isCancel(confirmed) || !confirmed) { p.cancel('Operazione annullata.'); return null; }
|
|
654
|
+
|
|
655
|
+
return { site, testReads, testWrites: doTestWrites, filterTier: tier, filterModule: filterMod };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// ── Main ─────────────────────────────────────────────────────────────
|
|
659
|
+
async function main() {
|
|
660
|
+
// ── Dual Mode Detection ────────────────────────────────────────
|
|
661
|
+
const isInteractive = process.argv.length === 2;
|
|
662
|
+
let config;
|
|
663
|
+
|
|
664
|
+
if (isInteractive) {
|
|
665
|
+
config = await interactiveMode();
|
|
666
|
+
if (!config) process.exit(0); // user cancelled
|
|
667
|
+
} else {
|
|
668
|
+
config = {
|
|
669
|
+
site: filterSite,
|
|
670
|
+
testReads: !testWrites, // default: read if not --test-writes
|
|
671
|
+
testWrites: testWrites,
|
|
672
|
+
filterTier,
|
|
673
|
+
filterModule,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
log('WP REST Bridge Validation Runner v' + RUNNER_VERSION);
|
|
678
|
+
|
|
679
|
+
// Ensure output directory exists
|
|
680
|
+
mkdirSync(resolve(PROJECT_ROOT, 'docs/validation'), { recursive: true });
|
|
681
|
+
|
|
682
|
+
// Import MCP SDK from server's node_modules
|
|
683
|
+
const { Client } = await import(resolve(SDK_PATH, 'dist/esm/client/index.js'));
|
|
684
|
+
const { StdioClientTransport } = await import(resolve(SDK_PATH, 'dist/esm/client/stdio.js'));
|
|
685
|
+
|
|
686
|
+
const serverPath = resolve(SERVER_DIR, 'build/server.js');
|
|
687
|
+
|
|
688
|
+
log(`Spawning server: node ${serverPath}`);
|
|
689
|
+
const transport = new StdioClientTransport({
|
|
690
|
+
command: 'node',
|
|
691
|
+
args: [serverPath],
|
|
692
|
+
env: { ...process.env },
|
|
693
|
+
});
|
|
694
|
+
|
|
695
|
+
const client = new Client({ name: 'validation-runner', version: RUNNER_VERSION });
|
|
696
|
+
|
|
697
|
+
try {
|
|
698
|
+
await client.connect(transport);
|
|
699
|
+
log('Connected to MCP server');
|
|
700
|
+
|
|
701
|
+
// Switch site if specified (interactive or --site flag)
|
|
702
|
+
if (config.site) {
|
|
703
|
+
log(`Switching to site: ${config.site}`);
|
|
704
|
+
await client.callTool({ name: 'switch_site', arguments: { site_id: config.site } });
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// List tools from server
|
|
708
|
+
const { tools: serverTools } = await client.listTools();
|
|
709
|
+
const serverToolNames = new Set(serverTools.map(t => t.name));
|
|
710
|
+
log(`Server reports ${serverToolNames.size} tools`);
|
|
711
|
+
|
|
712
|
+
// Warn about unregistered tools
|
|
713
|
+
for (const name of serverToolNames) {
|
|
714
|
+
if (!TOOL_REGISTRY.find(t => t.name === name)) {
|
|
715
|
+
log(`WARNING: Server tool "${name}" not in registry — add it to TOOL_REGISTRY`);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
for (const t of TOOL_REGISTRY) {
|
|
719
|
+
if (!serverToolNames.has(t.name)) {
|
|
720
|
+
log(`WARNING: Registry tool "${t.name}" not found on server`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
// Filter registry by module if requested
|
|
725
|
+
let registry = TOOL_REGISTRY;
|
|
726
|
+
if (config.filterModule) {
|
|
727
|
+
registry = registry.filter(t => t.module === config.filterModule);
|
|
728
|
+
log(`Filtered to module "${config.filterModule}": ${registry.length} tools`);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// ── Service Detection ──────────────────────────────────────────
|
|
732
|
+
log('Detecting service availability...');
|
|
733
|
+
const services = {};
|
|
734
|
+
for (const [service, probeTool] of Object.entries(SERVICE_PROBES)) {
|
|
735
|
+
if (!serverToolNames.has(probeTool)) {
|
|
736
|
+
services[service] = { configured: false, reason: 'probe tool not on server' };
|
|
737
|
+
continue;
|
|
738
|
+
}
|
|
739
|
+
try {
|
|
740
|
+
const probeEntry = TOOL_REGISTRY.find(t => t.name === probeTool);
|
|
741
|
+
const probeArgs = probeEntry?.safeArgs || {};
|
|
742
|
+
const result = await client.callTool({ name: probeTool, arguments: probeArgs });
|
|
743
|
+
const text = result.content?.map(c => c.text || '').join(' ') || '';
|
|
744
|
+
if (text.includes('not configured') || text.includes('Not configured')) {
|
|
745
|
+
services[service] = { configured: false, reason: truncate(text, 100) };
|
|
746
|
+
} else if (result.isError) {
|
|
747
|
+
services[service] = { configured: false, reason: truncate(text, 100) };
|
|
748
|
+
} else {
|
|
749
|
+
services[service] = { configured: true };
|
|
750
|
+
}
|
|
751
|
+
} catch (err) {
|
|
752
|
+
services[service] = { configured: false, reason: truncate(err.message, 100) };
|
|
753
|
+
}
|
|
754
|
+
await sleep(delay);
|
|
755
|
+
}
|
|
756
|
+
log('Service detection complete: ' + Object.entries(services).map(([k, v]) => `${k}=${v.configured ? 'OK' : 'NO'}`).join(', '));
|
|
757
|
+
|
|
758
|
+
// ── Write Sequence Testing (--test-writes or interactive) ───
|
|
759
|
+
if (config.testWrites) {
|
|
760
|
+
log('\n=== WRITE TOOL TESTING (CRUD Sequences) ===');
|
|
761
|
+
const writeResults = await executeWriteSequences(client, services, delay, config.filterTier);
|
|
762
|
+
|
|
763
|
+
// Merge write results into existing results.json
|
|
764
|
+
let existing = { meta: {}, services: {}, modules: {}, summary: { by_status: {} } };
|
|
765
|
+
if (existsSync(RESULTS_PATH)) {
|
|
766
|
+
try {
|
|
767
|
+
existing = JSON.parse(readFileSync(RESULTS_PATH, 'utf-8'));
|
|
768
|
+
} catch { /* start fresh if corrupted */ }
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Update meta
|
|
772
|
+
existing.meta = {
|
|
773
|
+
...existing.meta,
|
|
774
|
+
generated_at: new Date().toISOString(),
|
|
775
|
+
runner_version: RUNNER_VERSION,
|
|
776
|
+
last_write_test: new Date().toISOString(),
|
|
777
|
+
write_test_tier: config.filterTier || 'all',
|
|
778
|
+
};
|
|
779
|
+
existing.services = { ...existing.services, ...services };
|
|
780
|
+
|
|
781
|
+
// Update individual tool results within modules
|
|
782
|
+
for (const wr of writeResults) {
|
|
783
|
+
// Find which module this tool belongs to
|
|
784
|
+
const regEntry = TOOL_REGISTRY.find(t => t.name === wr.name);
|
|
785
|
+
if (!regEntry) continue;
|
|
786
|
+
const mod = regEntry.module;
|
|
787
|
+
if (!existing.modules[mod]) {
|
|
788
|
+
existing.modules[mod] = { service: regEntry.service, tools: [] };
|
|
789
|
+
}
|
|
790
|
+
// Replace existing result for this tool, or add new
|
|
791
|
+
const idx = existing.modules[mod].tools.findIndex(t => t.name === wr.name);
|
|
792
|
+
if (idx >= 0) {
|
|
793
|
+
existing.modules[mod].tools[idx] = wr;
|
|
794
|
+
} else {
|
|
795
|
+
existing.modules[mod].tools.push(wr);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// Recalculate summary
|
|
800
|
+
const allTools = Object.values(existing.modules).flatMap(m => m.tools);
|
|
801
|
+
existing.summary.by_status = {};
|
|
802
|
+
for (const status of ['passed', 'failed', 'error', 'not_configured', 'skipped_write', 'skipped', 'untested']) {
|
|
803
|
+
existing.summary.by_status[status] = allTools.filter(r => r.status === status).length;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
writeFileSync(RESULTS_PATH, JSON.stringify(existing, null, 2));
|
|
807
|
+
generateMarkdown(existing);
|
|
808
|
+
|
|
809
|
+
// Print summary
|
|
810
|
+
const wPassed = writeResults.filter(r => r.status === 'passed').length;
|
|
811
|
+
const wFailed = writeResults.filter(r => r.status === 'failed').length;
|
|
812
|
+
const wError = writeResults.filter(r => r.status === 'error').length;
|
|
813
|
+
const wSkipped = writeResults.filter(r => r.status === 'skipped').length;
|
|
814
|
+
log(`\nWrite test summary: ${wPassed} passed, ${wFailed} failed, ${wError} errors, ${wSkipped} skipped`);
|
|
815
|
+
log(`Results merged into ${RESULTS_PATH}`);
|
|
816
|
+
log(`Markdown updated at ${VALIDATION_MD_PATH}`);
|
|
817
|
+
|
|
818
|
+
// If not also testing reads, we're done
|
|
819
|
+
if (!config.testReads) {
|
|
820
|
+
await client.close();
|
|
821
|
+
return;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
// ── Dynamic Args Resolution ──────────────────────────────────
|
|
826
|
+
// For tools marked safeArgs:'dynamic', discover real IDs from the live site
|
|
827
|
+
const dynamicArgs = {};
|
|
828
|
+
if (services.wordpress_core?.configured) {
|
|
829
|
+
log('Resolving dynamic test arguments...');
|
|
830
|
+
try {
|
|
831
|
+
// Discover a real post ID and slug
|
|
832
|
+
const postsResult = await client.callTool({ name: 'list_content', arguments: { content_type: 'post', per_page: 1 } });
|
|
833
|
+
const postsText = postsResult.content?.[0]?.text || '';
|
|
834
|
+
const postId = JSON.parse(postsText)?.[0]?.id;
|
|
835
|
+
const postSlug = JSON.parse(postsText)?.[0]?.slug;
|
|
836
|
+
if (postId) {
|
|
837
|
+
dynamicArgs.get_content = { content_type: 'post', id: postId };
|
|
838
|
+
dynamicArgs.get_content_terms = { content_id: postId, content_type: 'post' };
|
|
839
|
+
log(` Post ID=${postId}, slug="${postSlug}"`);
|
|
840
|
+
}
|
|
841
|
+
if (postSlug) {
|
|
842
|
+
dynamicArgs.get_content_by_slug = { slug: postSlug, content_types: ['post'] };
|
|
843
|
+
dynamicArgs.find_content_by_url = { url: '/' + postSlug + '/' };
|
|
844
|
+
}
|
|
845
|
+
await sleep(delay);
|
|
846
|
+
|
|
847
|
+
// Discover a real comment ID
|
|
848
|
+
const commentsResult = await client.callTool({ name: 'list_comments', arguments: { per_page: 1 } });
|
|
849
|
+
const commentsText = commentsResult.content?.[0]?.text || '';
|
|
850
|
+
const commentId = JSON.parse(commentsText)?.[0]?.id;
|
|
851
|
+
if (commentId) {
|
|
852
|
+
dynamicArgs.get_comment = { id: commentId };
|
|
853
|
+
log(` Comment ID=${commentId}`);
|
|
854
|
+
}
|
|
855
|
+
await sleep(delay);
|
|
856
|
+
|
|
857
|
+
// Discover a real plugin name
|
|
858
|
+
const pluginsResult = await client.callTool({ name: 'list_plugins', arguments: { status: 'active' } });
|
|
859
|
+
const pluginsText = pluginsResult.content?.[0]?.text || '';
|
|
860
|
+
try {
|
|
861
|
+
const plugins = JSON.parse(pluginsText);
|
|
862
|
+
const firstPlugin = Array.isArray(plugins) ? plugins[0] : Object.values(plugins)[0];
|
|
863
|
+
const pluginSlug = firstPlugin?.plugin || firstPlugin?.textdomain;
|
|
864
|
+
if (pluginSlug) {
|
|
865
|
+
dynamicArgs.get_plugin = { plugin: pluginSlug };
|
|
866
|
+
log(` Plugin="${pluginSlug}"`);
|
|
867
|
+
}
|
|
868
|
+
} catch { /* skip if parsing fails */ }
|
|
869
|
+
await sleep(delay);
|
|
870
|
+
|
|
871
|
+
// Discover a real media ID
|
|
872
|
+
const mediaResult = await client.callTool({ name: 'list_media', arguments: { per_page: 1 } });
|
|
873
|
+
const mediaText = mediaResult.content?.[0]?.text || '';
|
|
874
|
+
const mediaId = JSON.parse(mediaText)?.[0]?.id;
|
|
875
|
+
if (mediaId) {
|
|
876
|
+
dynamicArgs.get_media = { id: mediaId };
|
|
877
|
+
log(` Media ID=${mediaId}`);
|
|
878
|
+
}
|
|
879
|
+
await sleep(delay);
|
|
880
|
+
} catch (err) {
|
|
881
|
+
log(` Dynamic resolution error: ${err.message}`);
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// ── Tool Testing ───────────────────────────────────────────────
|
|
886
|
+
log('Testing tools...');
|
|
887
|
+
const toolResults = [];
|
|
888
|
+
|
|
889
|
+
for (const entry of registry) {
|
|
890
|
+
const toolResult = {
|
|
891
|
+
name: entry.name,
|
|
892
|
+
type: entry.type,
|
|
893
|
+
status: 'untested',
|
|
894
|
+
tested_at: null,
|
|
895
|
+
duration_ms: null,
|
|
896
|
+
response_preview: null,
|
|
897
|
+
error_message: null,
|
|
898
|
+
skip_reason: null,
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// Skip writes unless --include-writes or interactive "all" mode
|
|
902
|
+
if (entry.type === 'write' && !includeWrites && !config.testWrites) {
|
|
903
|
+
toolResult.status = 'skipped_write';
|
|
904
|
+
toolResult.skip_reason = 'Write tool — use --include-writes to test';
|
|
905
|
+
toolResults.push(toolResult);
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Skip if service not configured
|
|
910
|
+
const svc = services[entry.service];
|
|
911
|
+
if (svc && !svc.configured) {
|
|
912
|
+
toolResult.status = 'not_configured';
|
|
913
|
+
toolResult.skip_reason = `Service "${entry.service}" not configured`;
|
|
914
|
+
toolResults.push(toolResult);
|
|
915
|
+
continue;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Resolve dynamic safeArgs
|
|
919
|
+
let resolvedArgs = entry.safeArgs;
|
|
920
|
+
if (entry.safeArgs === 'dynamic') {
|
|
921
|
+
resolvedArgs = dynamicArgs[entry.name] || null;
|
|
922
|
+
if (!resolvedArgs) {
|
|
923
|
+
toolResult.status = 'skipped';
|
|
924
|
+
toolResult.skip_reason = 'No data found for dynamic args resolution';
|
|
925
|
+
toolResults.push(toolResult);
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Skip if no safeArgs defined for read tool
|
|
931
|
+
if (!resolvedArgs) {
|
|
932
|
+
toolResult.status = 'skipped';
|
|
933
|
+
toolResult.skip_reason = 'No safe test arguments defined';
|
|
934
|
+
toolResults.push(toolResult);
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
// Skip if tool not on server
|
|
939
|
+
if (!serverToolNames.has(entry.name)) {
|
|
940
|
+
toolResult.status = 'skipped';
|
|
941
|
+
toolResult.skip_reason = 'Tool not registered on server';
|
|
942
|
+
toolResults.push(toolResult);
|
|
943
|
+
continue;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Execute tool call
|
|
947
|
+
const startTime = Date.now();
|
|
948
|
+
try {
|
|
949
|
+
const result = await Promise.race([
|
|
950
|
+
client.callTool({ name: entry.name, arguments: resolvedArgs }),
|
|
951
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), TIMEOUT_MS)),
|
|
952
|
+
]);
|
|
953
|
+
const elapsed = Date.now() - startTime;
|
|
954
|
+
const text = result.content?.map(c => c.text || '').join(' ') || '';
|
|
955
|
+
|
|
956
|
+
toolResult.tested_at = new Date().toISOString();
|
|
957
|
+
toolResult.duration_ms = elapsed;
|
|
958
|
+
toolResult.response_preview = truncate(text);
|
|
959
|
+
|
|
960
|
+
if (result.isError) {
|
|
961
|
+
toolResult.status = text.includes('not configured') || text.includes('Not configured')
|
|
962
|
+
? 'not_configured' : 'failed';
|
|
963
|
+
toolResult.error_message = truncate(text);
|
|
964
|
+
} else {
|
|
965
|
+
toolResult.status = 'passed';
|
|
966
|
+
}
|
|
967
|
+
} catch (err) {
|
|
968
|
+
toolResult.tested_at = new Date().toISOString();
|
|
969
|
+
toolResult.duration_ms = Date.now() - startTime;
|
|
970
|
+
toolResult.status = 'error';
|
|
971
|
+
toolResult.error_message = truncate(err.message);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const icon = { passed: '+', failed: 'X', error: '!', not_configured: '~' }[toolResult.status] || '?';
|
|
975
|
+
log(` [${icon}] ${entry.name} (${toolResult.duration_ms}ms)`);
|
|
976
|
+
toolResults.push(toolResult);
|
|
977
|
+
await sleep(delay);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
// ── Build Results ──────────────────────────────────────────────
|
|
981
|
+
// Group by module
|
|
982
|
+
const modules = {};
|
|
983
|
+
for (const entry of registry) {
|
|
984
|
+
if (!modules[entry.module]) {
|
|
985
|
+
modules[entry.module] = { service: entry.service, tools: [] };
|
|
986
|
+
}
|
|
987
|
+
const result = toolResults.find(r => r.name === entry.name);
|
|
988
|
+
if (result) modules[entry.module].tools.push(result);
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
// Summary
|
|
992
|
+
const summary = { by_status: {} };
|
|
993
|
+
for (const status of ['passed', 'failed', 'error', 'not_configured', 'skipped_write', 'skipped', 'untested']) {
|
|
994
|
+
summary.by_status[status] = toolResults.filter(r => r.status === status).length;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Get active site
|
|
998
|
+
let activeSite = 'unknown';
|
|
999
|
+
try {
|
|
1000
|
+
const siteResult = await client.callTool({ name: 'get_active_site', arguments: {} });
|
|
1001
|
+
activeSite = siteResult.content?.[0]?.text || 'unknown';
|
|
1002
|
+
} catch { /* ignore */ }
|
|
1003
|
+
|
|
1004
|
+
const results = {
|
|
1005
|
+
meta: {
|
|
1006
|
+
generated_at: new Date().toISOString(),
|
|
1007
|
+
active_site: activeSite,
|
|
1008
|
+
total_tools_registered: TOOL_REGISTRY.length,
|
|
1009
|
+
total_tools_on_server: serverToolNames.size,
|
|
1010
|
+
runner_version: RUNNER_VERSION,
|
|
1011
|
+
},
|
|
1012
|
+
services,
|
|
1013
|
+
modules,
|
|
1014
|
+
summary,
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// ── Merge incrementale ─────────────────────────────────────────
|
|
1018
|
+
if (config.filterModule && existsSync(RESULTS_PATH)) {
|
|
1019
|
+
log(`Merging results for module "${config.filterModule}" into existing results.json`);
|
|
1020
|
+
const existing = JSON.parse(readFileSync(RESULTS_PATH, 'utf-8'));
|
|
1021
|
+
existing.meta = results.meta;
|
|
1022
|
+
existing.services = { ...existing.services, ...results.services };
|
|
1023
|
+
for (const [mod, data] of Object.entries(results.modules)) {
|
|
1024
|
+
existing.modules[mod] = data;
|
|
1025
|
+
}
|
|
1026
|
+
// Recalculate summary from all modules
|
|
1027
|
+
const allTools = Object.values(existing.modules).flatMap(m => m.tools);
|
|
1028
|
+
for (const status of Object.keys(existing.summary.by_status)) {
|
|
1029
|
+
existing.summary.by_status[status] = allTools.filter(r => r.status === status).length;
|
|
1030
|
+
}
|
|
1031
|
+
writeFileSync(RESULTS_PATH, JSON.stringify(existing, null, 2));
|
|
1032
|
+
generateMarkdown(existing);
|
|
1033
|
+
} else {
|
|
1034
|
+
writeFileSync(RESULTS_PATH, JSON.stringify(results, null, 2));
|
|
1035
|
+
generateMarkdown(results);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
log(`Results written to ${RESULTS_PATH}`);
|
|
1039
|
+
log(`Markdown written to ${VALIDATION_MD_PATH}`);
|
|
1040
|
+
log(`Summary: ${Object.entries(summary.by_status).map(([k, v]) => `${k}=${v}`).join(', ')}`);
|
|
1041
|
+
|
|
1042
|
+
await client.close();
|
|
1043
|
+
} catch (err) {
|
|
1044
|
+
log(`Fatal error: ${err.message}`);
|
|
1045
|
+
try { await client.close(); } catch { /* ignore */ }
|
|
1046
|
+
process.exit(1);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// ── Markdown Generator ─────────────────────────────────────────────
|
|
1051
|
+
function generateMarkdown(results) {
|
|
1052
|
+
const { meta, services, modules, summary } = results;
|
|
1053
|
+
const lines = [];
|
|
1054
|
+
|
|
1055
|
+
lines.push('# WP REST Bridge — Validation Report');
|
|
1056
|
+
lines.push('');
|
|
1057
|
+
lines.push(`> Generated: ${meta.generated_at} `);
|
|
1058
|
+
lines.push(`> Active site: \`${meta.active_site}\` `);
|
|
1059
|
+
lines.push(`> Tools registered: ${meta.total_tools_registered} | On server: ${meta.total_tools_on_server} `);
|
|
1060
|
+
lines.push(`> Runner: v${meta.runner_version}`);
|
|
1061
|
+
lines.push('');
|
|
1062
|
+
|
|
1063
|
+
// Service Configuration
|
|
1064
|
+
lines.push('## Service Configuration');
|
|
1065
|
+
lines.push('');
|
|
1066
|
+
lines.push('| Service | Status | Note |');
|
|
1067
|
+
lines.push('|---------|--------|------|');
|
|
1068
|
+
for (const [svc, info] of Object.entries(services)) {
|
|
1069
|
+
const icon = info.configured ? 'OK' : 'NO';
|
|
1070
|
+
lines.push(`| ${svc} | ${icon} | ${info.reason || ''} |`);
|
|
1071
|
+
}
|
|
1072
|
+
lines.push('');
|
|
1073
|
+
|
|
1074
|
+
// Summary
|
|
1075
|
+
lines.push('## Summary');
|
|
1076
|
+
lines.push('');
|
|
1077
|
+
lines.push('| Status | Count |');
|
|
1078
|
+
lines.push('|--------|-------|');
|
|
1079
|
+
for (const [status, count] of Object.entries(summary.by_status)) {
|
|
1080
|
+
lines.push(`| ${status} | ${count} |`);
|
|
1081
|
+
}
|
|
1082
|
+
const total = Object.values(summary.by_status).reduce((a, b) => a + b, 0);
|
|
1083
|
+
lines.push(`| **Total** | **${total}** |`);
|
|
1084
|
+
lines.push('');
|
|
1085
|
+
|
|
1086
|
+
// Tool Inventory by Module
|
|
1087
|
+
lines.push('## Tool Inventory by Module');
|
|
1088
|
+
lines.push('');
|
|
1089
|
+
for (const [mod, data] of Object.entries(modules)) {
|
|
1090
|
+
lines.push(`### ${mod} (${data.service})`);
|
|
1091
|
+
lines.push('');
|
|
1092
|
+
lines.push('| Tool | Type | Status | Tested | Note |');
|
|
1093
|
+
lines.push('|------|------|--------|--------|------|');
|
|
1094
|
+
for (const t of data.tools) {
|
|
1095
|
+
const date = t.tested_at ? t.tested_at.split('T')[0] : '';
|
|
1096
|
+
const dur = t.duration_ms != null ? `${t.duration_ms}ms` : '';
|
|
1097
|
+
const note = t.error_message || t.skip_reason || (dur ? dur : '');
|
|
1098
|
+
lines.push(`| ${t.name} | ${t.type.toUpperCase()} | ${t.status} | ${date} | ${truncate(note, 80)} |`);
|
|
1099
|
+
}
|
|
1100
|
+
lines.push('');
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// Failed Tools Detail
|
|
1104
|
+
const failed = Object.values(modules)
|
|
1105
|
+
.flatMap(m => m.tools)
|
|
1106
|
+
.filter(t => t.status === 'failed' || t.status === 'error');
|
|
1107
|
+
if (failed.length > 0) {
|
|
1108
|
+
lines.push('## Failed Tools Detail');
|
|
1109
|
+
lines.push('');
|
|
1110
|
+
for (const t of failed) {
|
|
1111
|
+
lines.push(`### ${t.name} (${t.status})`);
|
|
1112
|
+
lines.push(`- **Tested**: ${t.tested_at}`);
|
|
1113
|
+
lines.push(`- **Duration**: ${t.duration_ms}ms`);
|
|
1114
|
+
lines.push(`- **Error**: ${t.error_message}`);
|
|
1115
|
+
if (t.response_preview) lines.push(`- **Response**: ${t.response_preview}`);
|
|
1116
|
+
lines.push('');
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Changelog
|
|
1121
|
+
lines.push('## Changelog');
|
|
1122
|
+
lines.push('');
|
|
1123
|
+
lines.push(`- ${meta.generated_at} — Run on \`${meta.active_site}\`: ${Object.entries(summary.by_status).map(([k, v]) => `${k}=${v}`).join(', ')}`);
|
|
1124
|
+
lines.push('');
|
|
1125
|
+
|
|
1126
|
+
writeFileSync(VALIDATION_MD_PATH, lines.join('\n'));
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
main().catch(err => {
|
|
1130
|
+
log(`Unhandled error: ${err.message}`);
|
|
1131
|
+
process.exit(1);
|
|
1132
|
+
});
|