@adsim/wordpress-mcp-server 4.5.0 → 4.6.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/README.md +92 -11
- package/dxt/manifest.json +17 -6
- package/index.js +327 -164
- package/package.json +1 -1
- package/src/plugins/IPluginAdapter.js +95 -0
- package/src/plugins/adapters/acf/acfAdapter.js +181 -0
- package/src/plugins/adapters/elementor/elementorAdapter.js +176 -0
- package/src/plugins/contextGuard.js +57 -0
- package/src/plugins/registry.js +94 -0
- package/tests/unit/pluginLayer.test.js +151 -0
- package/tests/unit/plugins/acf/acfAdapter.test.js +205 -0
- package/tests/unit/plugins/acf/acfAdapter.write.test.js +157 -0
- package/tests/unit/plugins/contextGuard.test.js +51 -0
- package/tests/unit/plugins/elementor/elementorAdapter.test.js +206 -0
- package/tests/unit/plugins/iPluginAdapter.test.js +34 -0
- package/tests/unit/plugins/registry.test.js +84 -0
- package/tests/unit/tools/dynamicFiltering.test.js +136 -0
- package/tests/unit/tools/outputCompression.test.js +342 -0
- package/tests/unit/tools/site.test.js +3 -1
- package/tests/unit/tools/siteOptions.test.js +101 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { elementorAdapter } from '../../../../src/plugins/adapters/elementor/elementorAdapter.js';
|
|
3
|
+
import { validateAdapter } from '../../../../src/plugins/IPluginAdapter.js';
|
|
4
|
+
import { PluginRegistry } from '../../../../src/plugins/registry.js';
|
|
5
|
+
|
|
6
|
+
let consoleSpy;
|
|
7
|
+
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
consoleSpy.mockRestore();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// ── Helpers ──
|
|
16
|
+
|
|
17
|
+
function parseResult(result) {
|
|
18
|
+
return JSON.parse(result?.content?.[0]?.text);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getAuditLogs() {
|
|
22
|
+
return consoleSpy.mock.calls
|
|
23
|
+
.map(c => c[0])
|
|
24
|
+
.filter(m => typeof m === 'string' && m.startsWith('[AUDIT]'))
|
|
25
|
+
.map(m => JSON.parse(m.replace('[AUDIT] ', '')));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function mockApi(data, shouldThrow = false) {
|
|
29
|
+
if (shouldThrow) {
|
|
30
|
+
return async () => { throw new Error('WP API 404: Not Found'); };
|
|
31
|
+
}
|
|
32
|
+
return async () => data;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ── Mock data ──
|
|
36
|
+
|
|
37
|
+
const mockTemplates = [
|
|
38
|
+
{ id: 1, title: { rendered: 'Hero Template' }, type: 'page', author: 1, date: '2024-06-01', source: 'local' },
|
|
39
|
+
{ id: 2, title: { rendered: 'CTA Section' }, type: 'section', author: 1, date: '2024-06-02', source: 'local' },
|
|
40
|
+
{ id: 3, title: { rendered: 'Header Block' }, type: 'block', author: 2, date: '2024-06-03', source: 'remote' },
|
|
41
|
+
{ id: 4, title: { rendered: 'Promo Popup' }, type: 'popup', author: 1, date: '2024-06-04', source: 'local' },
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const mockTemplateDetail = {
|
|
45
|
+
id: 1,
|
|
46
|
+
title: { rendered: 'Hero Template' },
|
|
47
|
+
type: 'page',
|
|
48
|
+
content: {
|
|
49
|
+
elements: [
|
|
50
|
+
{ elType: 'section', elements: [{ elType: 'widget', widgetType: 'heading' }] },
|
|
51
|
+
{ elType: 'section', elements: [{ elType: 'widget', widgetType: 'image' }] },
|
|
52
|
+
],
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const mockPageData = {
|
|
57
|
+
elements: [
|
|
58
|
+
{ elType: 'section', widgetType: null, elements: [
|
|
59
|
+
{ elType: 'column', elements: [
|
|
60
|
+
{ elType: 'widget', widgetType: 'heading', elements: [] },
|
|
61
|
+
{ elType: 'widget', widgetType: 'text-editor', elements: [] },
|
|
62
|
+
]},
|
|
63
|
+
]},
|
|
64
|
+
{ elType: 'section', widgetType: null, elements: [
|
|
65
|
+
{ elType: 'column', elements: [
|
|
66
|
+
{ elType: 'widget', widgetType: 'image', elements: [] },
|
|
67
|
+
{ elType: 'widget', widgetType: 'heading', elements: [] },
|
|
68
|
+
]},
|
|
69
|
+
]},
|
|
70
|
+
],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// =========================================================================
|
|
74
|
+
// Tests
|
|
75
|
+
// =========================================================================
|
|
76
|
+
|
|
77
|
+
describe('Elementor Adapter', () => {
|
|
78
|
+
|
|
79
|
+
// 1. elementor_list_templates → retourne liste avec id/title/type
|
|
80
|
+
it('elementor_list_templates returns list with id/title/type', async () => {
|
|
81
|
+
const tool = elementorAdapter.getTools().find(t => t.name === 'elementor_list_templates');
|
|
82
|
+
const result = await tool.handler({}, mockApi(mockTemplates));
|
|
83
|
+
const data = parseResult(result);
|
|
84
|
+
expect(data.total).toBe(4);
|
|
85
|
+
expect(data.templates[0].id).toBe(1);
|
|
86
|
+
expect(data.templates[0].title).toBe('Hero Template');
|
|
87
|
+
expect(data.templates[0].type).toBe('page');
|
|
88
|
+
expect(data.templates[2].source).toBe('remote');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// 2. elementor_list_templates avec type filter → filtre appliqué
|
|
92
|
+
it('elementor_list_templates filters by type', async () => {
|
|
93
|
+
const tool = elementorAdapter.getTools().find(t => t.name === 'elementor_list_templates');
|
|
94
|
+
const result = await tool.handler({ type: 'section' }, mockApi(mockTemplates));
|
|
95
|
+
const data = parseResult(result);
|
|
96
|
+
expect(data.total).toBe(1);
|
|
97
|
+
expect(data.templates[0].id).toBe(2);
|
|
98
|
+
expect(data.templates[0].type).toBe('section');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 3. elementor_get_template → retourne contenu complet
|
|
102
|
+
it('elementor_get_template returns full template content', async () => {
|
|
103
|
+
const tool = elementorAdapter.getTools().find(t => t.name === 'elementor_get_template');
|
|
104
|
+
const result = await tool.handler({ id: 1 }, mockApi(mockTemplateDetail));
|
|
105
|
+
const data = parseResult(result);
|
|
106
|
+
expect(data.id).toBe(1);
|
|
107
|
+
expect(data.title).toBe('Hero Template');
|
|
108
|
+
expect(data.type).toBe('page');
|
|
109
|
+
expect(data.content).toBeDefined();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 4. elementor_get_template → contextGuard truncate si > 50k
|
|
113
|
+
it('elementor_get_template applies contextGuard on large responses', async () => {
|
|
114
|
+
// Generate a template with > 50000 chars of content
|
|
115
|
+
const bigContent = { elements: [] };
|
|
116
|
+
for (let i = 0; i < 800; i++) {
|
|
117
|
+
bigContent.elements.push({ elType: 'widget', widgetType: 'text-editor', settings: { content: 'x'.repeat(100) } });
|
|
118
|
+
}
|
|
119
|
+
const bigTemplate = { id: 99, title: { rendered: 'Big Template' }, type: 'page', content: bigContent };
|
|
120
|
+
const tool = elementorAdapter.getTools().find(t => t.name === 'elementor_get_template');
|
|
121
|
+
const result = await tool.handler({ id: 99 }, mockApi(bigTemplate));
|
|
122
|
+
const data = parseResult(result);
|
|
123
|
+
expect(data._truncated).toBe(true);
|
|
124
|
+
expect(data._original_size).toBeGreaterThan(50000);
|
|
125
|
+
expect(data._warning).toContain('truncated');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 5. elementor_get_page_data → retourne widgets_used et elements_count
|
|
129
|
+
it('elementor_get_page_data returns widgets_used and elements_count', async () => {
|
|
130
|
+
const tool = elementorAdapter.getTools().find(t => t.name === 'elementor_get_page_data');
|
|
131
|
+
const result = await tool.handler({ post_id: 42 }, mockApi(mockPageData));
|
|
132
|
+
const data = parseResult(result);
|
|
133
|
+
expect(data.post_id).toBe(42);
|
|
134
|
+
expect(data.elementor_status).toBe('active');
|
|
135
|
+
expect(data.widgets_used).toContain('heading');
|
|
136
|
+
expect(data.widgets_used).toContain('text-editor');
|
|
137
|
+
expect(data.widgets_used).toContain('image');
|
|
138
|
+
expect(data.elements_count).toBeGreaterThan(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 6. elementor_get_template id invalide → erreur validation
|
|
142
|
+
it('elementor_get_template rejects invalid id', async () => {
|
|
143
|
+
const tool = elementorAdapter.getTools().find(t => t.name === 'elementor_get_template');
|
|
144
|
+
await expect(
|
|
145
|
+
tool.handler({ id: 'abc' }, mockApi({}))
|
|
146
|
+
).rejects.toThrow('Validation error');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 7. 404 template not found → erreur propre
|
|
150
|
+
it('elementor_get_template propagates 404 error', async () => {
|
|
151
|
+
const tool = elementorAdapter.getTools().find(t => t.name === 'elementor_get_template');
|
|
152
|
+
await expect(
|
|
153
|
+
tool.handler({ id: 99999 }, mockApi(null, true))
|
|
154
|
+
).rejects.toThrow('404');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// 8. Adapter détecté uniquement si namespace /elementor/v1 présent
|
|
158
|
+
it('registry detects elementor only when namespace is present', async () => {
|
|
159
|
+
const registry = new PluginRegistry();
|
|
160
|
+
|
|
161
|
+
// Without elementor namespace
|
|
162
|
+
await registry.initialize(mockApi({ namespaces: ['wp/v2'] }));
|
|
163
|
+
expect(registry.isActive('elementor')).toBe(false);
|
|
164
|
+
|
|
165
|
+
// With elementor namespace
|
|
166
|
+
registry.active.clear();
|
|
167
|
+
await registry.initialize(mockApi({ namespaces: ['wp/v2', 'elementor/v1'] }));
|
|
168
|
+
expect(registry.isActive('elementor')).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 9. Audit log format correct pour chaque outil
|
|
172
|
+
it('logs correct audit format for all 3 tools', async () => {
|
|
173
|
+
const listTool = elementorAdapter.getTools().find(t => t.name === 'elementor_list_templates');
|
|
174
|
+
const getTool = elementorAdapter.getTools().find(t => t.name === 'elementor_get_template');
|
|
175
|
+
const pageTool = elementorAdapter.getTools().find(t => t.name === 'elementor_get_page_data');
|
|
176
|
+
|
|
177
|
+
await listTool.handler({}, mockApi(mockTemplates));
|
|
178
|
+
await getTool.handler({ id: 1 }, mockApi(mockTemplateDetail));
|
|
179
|
+
await pageTool.handler({ post_id: 42 }, mockApi(mockPageData));
|
|
180
|
+
|
|
181
|
+
const logs = getAuditLogs();
|
|
182
|
+
|
|
183
|
+
const listEntry = logs.find(l => l.tool === 'elementor_list_templates');
|
|
184
|
+
expect(listEntry).toBeDefined();
|
|
185
|
+
expect(listEntry.action).toBe('list_templates');
|
|
186
|
+
expect(listEntry.status).toBe('success');
|
|
187
|
+
expect(typeof listEntry.latency_ms).toBe('number');
|
|
188
|
+
|
|
189
|
+
const getEntry = logs.find(l => l.tool === 'elementor_get_template');
|
|
190
|
+
expect(getEntry).toBeDefined();
|
|
191
|
+
expect(getEntry.action).toBe('read_template');
|
|
192
|
+
expect(getEntry.target).toBe(1);
|
|
193
|
+
|
|
194
|
+
const pageEntry = logs.find(l => l.tool === 'elementor_get_page_data');
|
|
195
|
+
expect(pageEntry).toBeDefined();
|
|
196
|
+
expect(pageEntry.action).toBe('read_page_data');
|
|
197
|
+
expect(pageEntry.target).toBe(42);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
// 10. Passes validateAdapter
|
|
201
|
+
it('passes validateAdapter without errors', () => {
|
|
202
|
+
const { valid, errors } = validateAdapter(elementorAdapter);
|
|
203
|
+
expect(valid).toBe(true);
|
|
204
|
+
expect(errors).toEqual([]);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { validateAdapter } from '../../../src/plugins/IPluginAdapter.js';
|
|
3
|
+
|
|
4
|
+
const validAdapter = {
|
|
5
|
+
id: 'acf',
|
|
6
|
+
namespace: 'acf/v3',
|
|
7
|
+
riskLevel: 'medium',
|
|
8
|
+
contextConfig: {
|
|
9
|
+
maxChars: 30000,
|
|
10
|
+
defaultMode: 'compact',
|
|
11
|
+
supportedModes: ['raw', 'compact', 'summary', 'ids_only'],
|
|
12
|
+
},
|
|
13
|
+
getTools: () => [],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
describe('validateAdapter', () => {
|
|
17
|
+
it('validates a complete adapter', () => {
|
|
18
|
+
const { valid, errors } = validateAdapter(validAdapter);
|
|
19
|
+
expect(valid).toBe(true);
|
|
20
|
+
expect(errors).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('rejects an adapter without id', () => {
|
|
24
|
+
const { valid, errors } = validateAdapter({ ...validAdapter, id: undefined });
|
|
25
|
+
expect(valid).toBe(false);
|
|
26
|
+
expect(errors.some(e => e.includes('"id"'))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('rejects an adapter without getTools', () => {
|
|
30
|
+
const { valid, errors } = validateAdapter({ ...validAdapter, getTools: undefined });
|
|
31
|
+
expect(valid).toBe(false);
|
|
32
|
+
expect(errors.some(e => e.includes('"getTools"'))).toBe(true);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
+
import { PluginRegistry } from '../../../src/plugins/registry.js';
|
|
3
|
+
|
|
4
|
+
let consoleSpy;
|
|
5
|
+
const envBackup = {};
|
|
6
|
+
|
|
7
|
+
function saveEnv(...keys) {
|
|
8
|
+
keys.forEach(k => { envBackup[k] = process.env[k]; });
|
|
9
|
+
}
|
|
10
|
+
function restoreEnv() {
|
|
11
|
+
Object.entries(envBackup).forEach(([k, v]) => {
|
|
12
|
+
if (v === undefined) delete process.env[k];
|
|
13
|
+
else process.env[k] = v;
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
19
|
+
saveEnv('WP_DISABLE_PLUGIN_LAYERS');
|
|
20
|
+
delete process.env.WP_DISABLE_PLUGIN_LAYERS;
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
consoleSpy.mockRestore();
|
|
24
|
+
restoreEnv();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function mockApiRequest(namespaces = []) {
|
|
28
|
+
return async () => ({ namespaces });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('PluginRegistry', () => {
|
|
32
|
+
it('initialize() detects ACF when "acf/v3" in namespaces', async () => {
|
|
33
|
+
const reg = new PluginRegistry();
|
|
34
|
+
await reg.initialize(mockApiRequest(['wp/v2', 'acf/v3']));
|
|
35
|
+
expect(reg.isActive('acf')).toBe(true);
|
|
36
|
+
expect(reg.isActive('elementor')).toBe(false);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('initialize() detects Elementor when "elementor/v1" in namespaces', async () => {
|
|
40
|
+
const reg = new PluginRegistry();
|
|
41
|
+
await reg.initialize(mockApiRequest(['wp/v2', 'elementor/v1', 'elementor/v2']));
|
|
42
|
+
expect(reg.isActive('elementor')).toBe(true);
|
|
43
|
+
expect(reg.isActive('acf')).toBe(false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('initialize() detects nothing when namespaces is empty', async () => {
|
|
47
|
+
const reg = new PluginRegistry();
|
|
48
|
+
await reg.initialize(mockApiRequest([]));
|
|
49
|
+
expect(reg.isActive('acf')).toBe(false);
|
|
50
|
+
expect(reg.isActive('elementor')).toBe(false);
|
|
51
|
+
expect(reg.isActive('astra')).toBe(false);
|
|
52
|
+
expect(reg.getSummary().active).toEqual([]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('getAvailableTools() returns [] when WP_DISABLE_PLUGIN_LAYERS=true', async () => {
|
|
56
|
+
const reg = new PluginRegistry();
|
|
57
|
+
await reg.initialize(mockApiRequest(['acf/v3']));
|
|
58
|
+
reg.registerTools('acf', [{ name: 'acf_get_fields' }]);
|
|
59
|
+
|
|
60
|
+
process.env.WP_DISABLE_PLUGIN_LAYERS = 'true';
|
|
61
|
+
expect(reg.getAvailableTools()).toEqual([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('isActive() returns true for detected plugins and false otherwise', async () => {
|
|
65
|
+
const reg = new PluginRegistry();
|
|
66
|
+
await reg.initialize(mockApiRequest(['acf/v3', 'astra/v1']));
|
|
67
|
+
expect(reg.isActive('acf')).toBe(true);
|
|
68
|
+
expect(reg.isActive('astra')).toBe(true);
|
|
69
|
+
expect(reg.isActive('elementor')).toBe(false);
|
|
70
|
+
expect(reg.isActive('nonexistent')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('getSummary() returns the correct structure', async () => {
|
|
74
|
+
const reg = new PluginRegistry();
|
|
75
|
+
await reg.initialize(mockApiRequest(['elementor/v1', 'acf/v3']));
|
|
76
|
+
const summary = reg.getSummary();
|
|
77
|
+
expect(summary.active).toContain('acf');
|
|
78
|
+
expect(summary.active).toContain('elementor');
|
|
79
|
+
expect(summary.disabled_by_env).toBe(false);
|
|
80
|
+
|
|
81
|
+
process.env.WP_DISABLE_PLUGIN_LAYERS = 'true';
|
|
82
|
+
expect(reg.getSummary().disabled_by_env).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
vi.mock('node-fetch', () => ({ default: vi.fn() }));
|
|
4
|
+
|
|
5
|
+
import fetch from 'node-fetch';
|
|
6
|
+
import { handleToolCall, getFilteredTools } from '../../../index.js';
|
|
7
|
+
import { makeRequest, mockSuccess, mockError, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let consoleSpy;
|
|
14
|
+
const envBackup = {};
|
|
15
|
+
|
|
16
|
+
function saveEnv(...keys) {
|
|
17
|
+
keys.forEach(k => { envBackup[k] = process.env[k]; });
|
|
18
|
+
}
|
|
19
|
+
function restoreEnv() {
|
|
20
|
+
Object.entries(envBackup).forEach(([k, v]) => {
|
|
21
|
+
if (v === undefined) delete process.env[k];
|
|
22
|
+
else process.env[k] = v;
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
fetch.mockReset();
|
|
28
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
29
|
+
saveEnv('WC_CONSUMER_KEY', 'WP_REQUIRE_APPROVAL', 'WP_ENABLE_PLUGIN_INTELLIGENCE');
|
|
30
|
+
// Default: all optional features OFF
|
|
31
|
+
delete process.env.WC_CONSUMER_KEY;
|
|
32
|
+
delete process.env.WP_REQUIRE_APPROVAL;
|
|
33
|
+
delete process.env.WP_ENABLE_PLUGIN_INTELLIGENCE;
|
|
34
|
+
});
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
consoleSpy.mockRestore();
|
|
37
|
+
restoreEnv();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// =========================================================================
|
|
41
|
+
// WooCommerce filtering
|
|
42
|
+
// =========================================================================
|
|
43
|
+
|
|
44
|
+
describe('WooCommerce filtering', () => {
|
|
45
|
+
it('hides WooCommerce tools when WC_CONSUMER_KEY absent', () => {
|
|
46
|
+
const tools = getFilteredTools();
|
|
47
|
+
const wcTools = tools.filter(t => t.name.startsWith('wc_'));
|
|
48
|
+
expect(wcTools).toHaveLength(0);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('shows WooCommerce tools when WC_CONSUMER_KEY set', () => {
|
|
52
|
+
process.env.WC_CONSUMER_KEY = 'ck_test';
|
|
53
|
+
const tools = getFilteredTools();
|
|
54
|
+
const wcTools = tools.filter(t => t.name.startsWith('wc_'));
|
|
55
|
+
expect(wcTools.length).toBe(13);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// =========================================================================
|
|
60
|
+
// Editorial workflow filtering
|
|
61
|
+
// =========================================================================
|
|
62
|
+
|
|
63
|
+
describe('Editorial workflow filtering', () => {
|
|
64
|
+
it('hides editorial tools when WP_REQUIRE_APPROVAL not true', () => {
|
|
65
|
+
const tools = getFilteredTools();
|
|
66
|
+
const names = tools.map(t => t.name);
|
|
67
|
+
expect(names).not.toContain('wp_submit_for_review');
|
|
68
|
+
expect(names).not.toContain('wp_approve_post');
|
|
69
|
+
expect(names).not.toContain('wp_reject_post');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('shows editorial tools when WP_REQUIRE_APPROVAL=true', () => {
|
|
73
|
+
process.env.WP_REQUIRE_APPROVAL = 'true';
|
|
74
|
+
const tools = getFilteredTools();
|
|
75
|
+
const names = tools.map(t => t.name);
|
|
76
|
+
expect(names).toContain('wp_submit_for_review');
|
|
77
|
+
expect(names).toContain('wp_approve_post');
|
|
78
|
+
expect(names).toContain('wp_reject_post');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// =========================================================================
|
|
83
|
+
// Plugin Intelligence filtering
|
|
84
|
+
// =========================================================================
|
|
85
|
+
|
|
86
|
+
describe('Plugin Intelligence filtering', () => {
|
|
87
|
+
it('hides Plugin Intelligence when WP_ENABLE_PLUGIN_INTELLIGENCE not true', () => {
|
|
88
|
+
const tools = getFilteredTools();
|
|
89
|
+
const names = tools.map(t => t.name);
|
|
90
|
+
expect(names).not.toContain('wp_get_rendered_head');
|
|
91
|
+
expect(names).not.toContain('wp_audit_schema_plugins');
|
|
92
|
+
expect(names).not.toContain('wp_get_twitter_meta');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('shows Plugin Intelligence when WP_ENABLE_PLUGIN_INTELLIGENCE=true', () => {
|
|
96
|
+
process.env.WP_ENABLE_PLUGIN_INTELLIGENCE = 'true';
|
|
97
|
+
const tools = getFilteredTools();
|
|
98
|
+
const piNames = ['wp_get_rendered_head', 'wp_audit_rendered_seo', 'wp_get_pillar_content', 'wp_audit_schema_plugins', 'wp_get_seo_score', 'wp_get_twitter_meta'];
|
|
99
|
+
const names = tools.map(t => t.name);
|
|
100
|
+
piNames.forEach(n => expect(names).toContain(n));
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// =========================================================================
|
|
105
|
+
// Combined counts
|
|
106
|
+
// =========================================================================
|
|
107
|
+
|
|
108
|
+
describe('Combined filtering counts', () => {
|
|
109
|
+
it('returns all 86 tools when all features enabled', () => {
|
|
110
|
+
process.env.WC_CONSUMER_KEY = 'ck_test';
|
|
111
|
+
process.env.WP_REQUIRE_APPROVAL = 'true';
|
|
112
|
+
process.env.WP_ENABLE_PLUGIN_INTELLIGENCE = 'true';
|
|
113
|
+
expect(getFilteredTools()).toHaveLength(86);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('returns 64 tools with no optional features (86 - 13wc - 3editorial - 6pi)', () => {
|
|
117
|
+
expect(getFilteredTools()).toHaveLength(64);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// =========================================================================
|
|
122
|
+
// handleToolCall still works for filtered-out tools
|
|
123
|
+
// =========================================================================
|
|
124
|
+
|
|
125
|
+
describe('Filtered tools remain callable', () => {
|
|
126
|
+
it('handleToolCall works for a filtered-out WooCommerce tool (wc_list_products)', async () => {
|
|
127
|
+
// WC_CONSUMER_KEY is absent, so wc_list_products is filtered from listTools
|
|
128
|
+
const names = getFilteredTools().map(t => t.name);
|
|
129
|
+
expect(names).not.toContain('wc_list_products');
|
|
130
|
+
|
|
131
|
+
// But calling it directly should NOT throw "Unknown tool"
|
|
132
|
+
// It will fail with a WooCommerce credentials error, which is fine
|
|
133
|
+
const res = await call('wc_list_products');
|
|
134
|
+
expect(res.content[0].text).not.toContain('Unknown tool');
|
|
135
|
+
});
|
|
136
|
+
});
|