@adsim/wordpress-mcp-server 4.5.1 → 5.1.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/.env.example +18 -0
- package/README.md +857 -447
- package/companion/mcp-diagnostics.php +1184 -0
- package/dxt/manifest.json +718 -90
- package/index.js +188 -4747
- package/package.json +14 -6
- package/src/data/plugin-performance-data.json +59 -0
- 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/src/shared/api.js +79 -0
- package/src/shared/audit.js +39 -0
- package/src/shared/context.js +15 -0
- package/src/shared/governance.js +98 -0
- package/src/shared/utils.js +148 -0
- package/src/tools/comments.js +50 -0
- package/src/tools/content.js +353 -0
- package/src/tools/core.js +114 -0
- package/src/tools/editorial.js +634 -0
- package/src/tools/fse.js +370 -0
- package/src/tools/health.js +160 -0
- package/src/tools/index.js +96 -0
- package/src/tools/intelligence.js +2082 -0
- package/src/tools/links.js +118 -0
- package/src/tools/media.js +71 -0
- package/src/tools/performance.js +219 -0
- package/src/tools/plugins.js +368 -0
- package/src/tools/schema.js +417 -0
- package/src/tools/security.js +590 -0
- package/src/tools/seo.js +1633 -0
- package/src/tools/taxonomy.js +115 -0
- package/src/tools/users.js +188 -0
- package/src/tools/woocommerce.js +1008 -0
- package/src/tools/workflow.js +409 -0
- package/src/transport/http.js +39 -0
- package/tests/unit/helpers/pagination.test.js +43 -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/bulkUpdate.test.js +188 -0
- package/tests/unit/tools/diagnostics.test.js +397 -0
- package/tests/unit/tools/dynamicFiltering.test.js +100 -8
- package/tests/unit/tools/editorialIntelligence.test.js +817 -0
- package/tests/unit/tools/fse.test.js +548 -0
- package/tests/unit/tools/multilingual.test.js +653 -0
- package/tests/unit/tools/performance.test.js +351 -0
- package/tests/unit/tools/runWorkflow.test.js +150 -0
- package/tests/unit/tools/schema.test.js +477 -0
- package/tests/unit/tools/security.test.js +695 -0
- package/tests/unit/tools/site.test.js +1 -1
- package/tests/unit/tools/siteOptions.test.js +101 -0
- package/tests/unit/tools/users.crud.test.js +399 -0
- package/tests/unit/tools/validateBlocks.test.js +186 -0
- package/tests/unit/tools/visualStaging.test.js +271 -0
- package/tests/unit/tools/woocommerce.advanced.test.js +679 -0
|
@@ -0,0 +1,186 @@
|
|
|
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, _testSetTarget } from '../../../index.js';
|
|
7
|
+
import { makeRequest, mockSuccess, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let consoleSpy;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
fetch.mockReset();
|
|
17
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
18
|
+
_testSetTarget('site1', { url: 'https://example.com', auth: 'Basic dGVzdDp0ZXN0' });
|
|
19
|
+
delete process.env.WP_VALIDATE_BLOCKS;
|
|
20
|
+
delete process.env.WP_READ_ONLY;
|
|
21
|
+
});
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
consoleSpy.mockRestore();
|
|
24
|
+
delete process.env.WP_VALIDATE_BLOCKS;
|
|
25
|
+
delete process.env.WP_READ_ONLY;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// =========================================================================
|
|
29
|
+
// 1. Valid HTML → valid: true, errors: []
|
|
30
|
+
// =========================================================================
|
|
31
|
+
describe('wp_validate_block_structure', () => {
|
|
32
|
+
it('returns valid for well-formed Gutenberg blocks', async () => {
|
|
33
|
+
const content = [
|
|
34
|
+
'<!-- wp:paragraph -->',
|
|
35
|
+
'<p>Hello world</p>',
|
|
36
|
+
'<!-- /wp:paragraph -->',
|
|
37
|
+
'',
|
|
38
|
+
'<!-- wp:heading {"level":2} -->',
|
|
39
|
+
'<h2>Title</h2>',
|
|
40
|
+
'<!-- /wp:heading -->'
|
|
41
|
+
].join('\n');
|
|
42
|
+
|
|
43
|
+
const res = await call('wp_validate_block_structure', { content });
|
|
44
|
+
const data = parseResult(res);
|
|
45
|
+
expect(data.valid).toBe(true);
|
|
46
|
+
expect(data.errors).toHaveLength(0);
|
|
47
|
+
expect(data.blocks_found).toEqual(
|
|
48
|
+
expect.arrayContaining([
|
|
49
|
+
{ name: 'core/paragraph', count: 1 },
|
|
50
|
+
{ name: 'core/heading', count: 1 }
|
|
51
|
+
])
|
|
52
|
+
);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// =========================================================================
|
|
56
|
+
// 2. Unclosed block → error detected
|
|
57
|
+
// =========================================================================
|
|
58
|
+
it('detects unclosed block comment', async () => {
|
|
59
|
+
const content = [
|
|
60
|
+
'<!-- wp:paragraph -->',
|
|
61
|
+
'<p>No closing comment</p>'
|
|
62
|
+
].join('\n');
|
|
63
|
+
|
|
64
|
+
const res = await call('wp_validate_block_structure', { content });
|
|
65
|
+
const data = parseResult(res);
|
|
66
|
+
expect(data.valid).toBe(false);
|
|
67
|
+
expect(data.errors.some(e => e.type === 'unclosed_block')).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// =========================================================================
|
|
71
|
+
// 3. Malformed JSON in comment → error detected
|
|
72
|
+
// =========================================================================
|
|
73
|
+
it('detects malformed JSON in block attributes', async () => {
|
|
74
|
+
const content = [
|
|
75
|
+
'<!-- wp:heading {level:2} -->',
|
|
76
|
+
'<h2>Bad JSON</h2>',
|
|
77
|
+
'<!-- /wp:heading -->'
|
|
78
|
+
].join('\n');
|
|
79
|
+
|
|
80
|
+
const res = await call('wp_validate_block_structure', { content });
|
|
81
|
+
const data = parseResult(res);
|
|
82
|
+
expect(data.valid).toBe(false);
|
|
83
|
+
expect(data.errors.some(e => e.type === 'malformed_json')).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// =========================================================================
|
|
87
|
+
// 4. Invalid nesting → error detected
|
|
88
|
+
// =========================================================================
|
|
89
|
+
it('detects invalid nesting (paragraph inside paragraph)', async () => {
|
|
90
|
+
const content = [
|
|
91
|
+
'<!-- wp:paragraph -->',
|
|
92
|
+
'<p>Outer</p>',
|
|
93
|
+
'<!-- wp:paragraph -->',
|
|
94
|
+
'<p>Inner — invalid</p>',
|
|
95
|
+
'<!-- /wp:paragraph -->',
|
|
96
|
+
'<!-- /wp:paragraph -->'
|
|
97
|
+
].join('\n');
|
|
98
|
+
|
|
99
|
+
const res = await call('wp_validate_block_structure', { content });
|
|
100
|
+
const data = parseResult(res);
|
|
101
|
+
expect(data.valid).toBe(false);
|
|
102
|
+
expect(data.errors.some(e => e.type === 'invalid_nesting')).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// =========================================================================
|
|
106
|
+
// 5. strict: false → no error on missing attributes
|
|
107
|
+
// =========================================================================
|
|
108
|
+
it('does not error on missing attributes when strict=false', async () => {
|
|
109
|
+
const content = [
|
|
110
|
+
'<!-- wp:heading -->',
|
|
111
|
+
'<h2>No attributes</h2>',
|
|
112
|
+
'<!-- /wp:heading -->'
|
|
113
|
+
].join('\n');
|
|
114
|
+
|
|
115
|
+
const res = await call('wp_validate_block_structure', { content, strict: false });
|
|
116
|
+
const data = parseResult(res);
|
|
117
|
+
expect(data.valid).toBe(true);
|
|
118
|
+
expect(data.errors.filter(e => e.type === 'missing_attributes')).toHaveLength(0);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// =========================================================================
|
|
122
|
+
// 6. strict: true → error on missing attributes
|
|
123
|
+
// =========================================================================
|
|
124
|
+
it('errors on missing attributes when strict=true', async () => {
|
|
125
|
+
const content = [
|
|
126
|
+
'<!-- wp:heading -->',
|
|
127
|
+
'<h2>No attributes</h2>',
|
|
128
|
+
'<!-- /wp:heading -->'
|
|
129
|
+
].join('\n');
|
|
130
|
+
|
|
131
|
+
const res = await call('wp_validate_block_structure', { content, strict: true });
|
|
132
|
+
const data = parseResult(res);
|
|
133
|
+
expect(data.valid).toBe(false);
|
|
134
|
+
expect(data.errors.some(e => e.type === 'missing_attributes')).toBe(true);
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// =========================================================================
|
|
139
|
+
// 7-9. WP_VALIDATE_BLOCKS guard on wp_update_post
|
|
140
|
+
// =========================================================================
|
|
141
|
+
describe('WP_VALIDATE_BLOCKS guard', () => {
|
|
142
|
+
// 7. WP_VALIDATE_BLOCKS=true → blocks update if invalid content
|
|
143
|
+
it('blocks wp_update_post with invalid content when WP_VALIDATE_BLOCKS=true', async () => {
|
|
144
|
+
process.env.WP_VALIDATE_BLOCKS = 'true';
|
|
145
|
+
const invalidContent = '<!-- wp:paragraph -->\n<p>Unclosed block</p>';
|
|
146
|
+
|
|
147
|
+
const res = await call('wp_update_post', { id: 1, content: invalidContent });
|
|
148
|
+
expect(res.isError).toBe(true);
|
|
149
|
+
const data = JSON.parse(res.content[0].text);
|
|
150
|
+
expect(data.status).toBe('blocked');
|
|
151
|
+
expect(data.reason).toContain('WP_VALIDATE_BLOCKS');
|
|
152
|
+
expect(data.errors.length).toBeGreaterThan(0);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// 8. WP_VALIDATE_BLOCKS=true → allows update if valid content
|
|
156
|
+
it('allows wp_update_post with valid content when WP_VALIDATE_BLOCKS=true', async () => {
|
|
157
|
+
process.env.WP_VALIDATE_BLOCKS = 'true';
|
|
158
|
+
const validContent = '<!-- wp:paragraph -->\n<p>Valid</p>\n<!-- /wp:paragraph -->';
|
|
159
|
+
|
|
160
|
+
// POST /posts/1 → updated
|
|
161
|
+
mockSuccess({
|
|
162
|
+
id: 1, title: { rendered: 'Test' }, status: 'publish',
|
|
163
|
+
link: 'https://example.com/test/', modified: '2025-06-16'
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const res = await call('wp_update_post', { id: 1, content: validContent });
|
|
167
|
+
const data = parseResult(res);
|
|
168
|
+
expect(data.success).toBe(true);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
// 9. WP_VALIDATE_BLOCKS not set → no interception
|
|
172
|
+
it('does not intercept wp_update_post when WP_VALIDATE_BLOCKS is not set', async () => {
|
|
173
|
+
// No WP_VALIDATE_BLOCKS env var
|
|
174
|
+
const invalidContent = '<!-- wp:paragraph -->\n<p>Unclosed</p>';
|
|
175
|
+
|
|
176
|
+
// POST /posts/1 → updated normally despite invalid blocks
|
|
177
|
+
mockSuccess({
|
|
178
|
+
id: 1, title: { rendered: 'Test' }, status: 'publish',
|
|
179
|
+
link: 'https://example.com/test/', modified: '2025-06-16'
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const res = await call('wp_update_post', { id: 1, content: invalidContent });
|
|
183
|
+
const data = parseResult(res);
|
|
184
|
+
expect(data.success).toBe(true);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
@@ -0,0 +1,271 @@
|
|
|
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, _testSetTarget, TOOLS_DEFINITIONS } from '../../../index.js';
|
|
7
|
+
import { makeRequest, mockSuccess, parseResult } from '../../helpers/mockWpRequest.js';
|
|
8
|
+
|
|
9
|
+
function call(name, args = {}) {
|
|
10
|
+
return handleToolCall(makeRequest(name, args));
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let consoleSpy;
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
fetch.mockReset();
|
|
17
|
+
consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
18
|
+
_testSetTarget('site1', { url: 'https://example.com', auth: 'Basic dGVzdDp0ZXN0' });
|
|
19
|
+
// Reset env vars
|
|
20
|
+
delete process.env.WP_VISUAL_STAGING;
|
|
21
|
+
delete process.env.WP_READ_ONLY;
|
|
22
|
+
});
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
consoleSpy.mockRestore();
|
|
25
|
+
delete process.env.WP_VISUAL_STAGING;
|
|
26
|
+
delete process.env.WP_READ_ONLY;
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function mockPage(overrides = {}) {
|
|
30
|
+
const { title, content, excerpt, ...rest } = overrides;
|
|
31
|
+
return {
|
|
32
|
+
id: 42,
|
|
33
|
+
title: { rendered: title || 'Live Page' },
|
|
34
|
+
content: { rendered: content || '<p>Live content</p>' },
|
|
35
|
+
excerpt: { rendered: excerpt || '' },
|
|
36
|
+
status: 'publish',
|
|
37
|
+
slug: 'live-page',
|
|
38
|
+
link: 'https://example.com/live-page/',
|
|
39
|
+
date: '2025-06-15T10:00:00',
|
|
40
|
+
modified: '2025-06-15T10:00:00',
|
|
41
|
+
parent: 0,
|
|
42
|
+
template: '',
|
|
43
|
+
meta: {},
|
|
44
|
+
...rest
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function mockStagingDraft(overrides = {}) {
|
|
49
|
+
const { title, content, excerpt, ...rest } = overrides;
|
|
50
|
+
return {
|
|
51
|
+
id: 99,
|
|
52
|
+
title: { rendered: title || '[STAGING] Live Page' },
|
|
53
|
+
content: { rendered: content || '<p>Updated staging content</p>' },
|
|
54
|
+
excerpt: { rendered: excerpt || '' },
|
|
55
|
+
status: 'draft',
|
|
56
|
+
slug: 'staging-live-page',
|
|
57
|
+
link: 'https://example.com/?p=99',
|
|
58
|
+
date: '2025-06-16T10:00:00',
|
|
59
|
+
modified: '2025-06-16T12:00:00',
|
|
60
|
+
meta: { _staging_source_id: 42 },
|
|
61
|
+
...rest
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// =========================================================================
|
|
66
|
+
// 1. wp_create_staging_draft — creates draft with correct _staging_source_id
|
|
67
|
+
// =========================================================================
|
|
68
|
+
describe('wp_create_staging_draft', () => {
|
|
69
|
+
it('creates a staging draft from a published page', async () => {
|
|
70
|
+
// 1st call: GET /pages/42 → published page
|
|
71
|
+
mockSuccess(mockPage());
|
|
72
|
+
// 2nd call: GET /pages?status=draft... → no existing drafts
|
|
73
|
+
mockSuccess([]);
|
|
74
|
+
// 3rd call: POST /pages → created draft
|
|
75
|
+
mockSuccess(mockStagingDraft());
|
|
76
|
+
|
|
77
|
+
const res = await call('wp_create_staging_draft', { source_id: 42, post_type: 'page' });
|
|
78
|
+
const data = parseResult(res);
|
|
79
|
+
expect(data.status).toBe('created');
|
|
80
|
+
expect(data.staging_draft_id).toBe(99);
|
|
81
|
+
expect(data.source_id).toBe(42);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// 2. Returns already_exists if draft exists
|
|
85
|
+
it('returns already_exists if staging draft already exists', async () => {
|
|
86
|
+
mockSuccess(mockPage());
|
|
87
|
+
mockSuccess([mockStagingDraft()]);
|
|
88
|
+
|
|
89
|
+
const res = await call('wp_create_staging_draft', { source_id: 42 });
|
|
90
|
+
const data = parseResult(res);
|
|
91
|
+
expect(data.status).toBe('already_exists');
|
|
92
|
+
expect(data.staging_draft_id).toBe(99);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
// 3. Error if source is not published
|
|
96
|
+
it('throws error if source is not published', async () => {
|
|
97
|
+
mockSuccess(mockPage({ status: 'draft' }));
|
|
98
|
+
|
|
99
|
+
const res = await call('wp_create_staging_draft', { source_id: 42 });
|
|
100
|
+
expect(res.isError).toBe(true);
|
|
101
|
+
expect(res.content[0].text).toContain('Only published content can be staged');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// =========================================================================
|
|
106
|
+
// 4. wp_list_staging_drafts — returns list filtered by source_id
|
|
107
|
+
// =========================================================================
|
|
108
|
+
describe('wp_list_staging_drafts', () => {
|
|
109
|
+
it('returns staging drafts filtered by source_id', async () => {
|
|
110
|
+
// Posts search
|
|
111
|
+
mockSuccess([]);
|
|
112
|
+
// Pages search
|
|
113
|
+
mockSuccess([mockStagingDraft(), mockStagingDraft({ id: 100, meta: { _staging_source_id: 55 } })]);
|
|
114
|
+
|
|
115
|
+
const res = await call('wp_list_staging_drafts', { source_id: 42 });
|
|
116
|
+
const data = parseResult(res);
|
|
117
|
+
expect(data.total).toBe(1);
|
|
118
|
+
expect(data.staging_drafts[0].source_id).toBe(42);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// =========================================================================
|
|
123
|
+
// 5. wp_get_staging_preview_url — returns correct preview_url
|
|
124
|
+
// =========================================================================
|
|
125
|
+
describe('wp_get_staging_preview_url', () => {
|
|
126
|
+
it('returns preview URL for a staging draft', async () => {
|
|
127
|
+
mockSuccess(mockStagingDraft());
|
|
128
|
+
|
|
129
|
+
const res = await call('wp_get_staging_preview_url', { staging_id: 99 });
|
|
130
|
+
const data = parseResult(res);
|
|
131
|
+
expect(data.preview_url).toBe('https://example.com/?p=99&preview=true');
|
|
132
|
+
expect(data.source_id).toBe(42);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// 6. Error if not a staging draft
|
|
136
|
+
it('throws error if post is not a staging draft', async () => {
|
|
137
|
+
mockSuccess(mockPage({ id: 42, meta: {} }));
|
|
138
|
+
|
|
139
|
+
const res = await call('wp_get_staging_preview_url', { staging_id: 42 });
|
|
140
|
+
expect(res.isError).toBe(true);
|
|
141
|
+
expect(res.content[0].text).toContain('not a staging draft');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// =========================================================================
|
|
146
|
+
// 7-8. wp_merge_staging_to_live
|
|
147
|
+
// =========================================================================
|
|
148
|
+
describe('wp_merge_staging_to_live', () => {
|
|
149
|
+
// 7. confirm:false → dry-run
|
|
150
|
+
it('returns dry-run preview when confirm=false', async () => {
|
|
151
|
+
// GET /pages/99 → staging draft
|
|
152
|
+
mockSuccess(mockStagingDraft());
|
|
153
|
+
// GET /pages/42 → live page
|
|
154
|
+
mockSuccess(mockPage());
|
|
155
|
+
|
|
156
|
+
const res = await call('wp_merge_staging_to_live', { staging_id: 99, confirm: false });
|
|
157
|
+
const data = parseResult(res);
|
|
158
|
+
expect(data.status).toBe('dry_run');
|
|
159
|
+
expect(data.changes_preview).toBeDefined();
|
|
160
|
+
expect(data.changes_preview.title.from).toBe('Live Page');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// 8. confirm:true → merge + audit
|
|
164
|
+
it('merges staging draft to live when confirm=true', async () => {
|
|
165
|
+
// GET /pages/99 → staging draft
|
|
166
|
+
mockSuccess(mockStagingDraft());
|
|
167
|
+
// GET /pages/42 → live page (for comparison, though merge skips this in confirm path)
|
|
168
|
+
mockSuccess(mockPage());
|
|
169
|
+
// POST /pages/42 → updated live page
|
|
170
|
+
mockSuccess(mockPage({ modified: '2025-06-16T14:00:00' }));
|
|
171
|
+
// DELETE /pages/99 → deleted
|
|
172
|
+
mockSuccess({ deleted: true });
|
|
173
|
+
|
|
174
|
+
const res = await call('wp_merge_staging_to_live', { staging_id: 99, confirm: true });
|
|
175
|
+
const data = parseResult(res);
|
|
176
|
+
expect(data.status).toBe('merged');
|
|
177
|
+
expect(data.source_id).toBe(42);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
// 9. Blocked by WP_READ_ONLY
|
|
181
|
+
it('is blocked when WP_READ_ONLY=true', async () => {
|
|
182
|
+
process.env.WP_READ_ONLY = 'true';
|
|
183
|
+
|
|
184
|
+
const res = await call('wp_merge_staging_to_live', { staging_id: 99 });
|
|
185
|
+
expect(res.isError).toBe(true);
|
|
186
|
+
expect(res.content[0].text).toContain('READ-ONLY');
|
|
187
|
+
|
|
188
|
+
delete process.env.WP_READ_ONLY;
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// =========================================================================
|
|
193
|
+
// 10-11. wp_discard_staging_draft
|
|
194
|
+
// =========================================================================
|
|
195
|
+
describe('wp_discard_staging_draft', () => {
|
|
196
|
+
// 10. confirm:false → dry-run
|
|
197
|
+
it('returns dry-run when confirm=false', async () => {
|
|
198
|
+
mockSuccess(mockStagingDraft());
|
|
199
|
+
|
|
200
|
+
const res = await call('wp_discard_staging_draft', { staging_id: 99, confirm: false });
|
|
201
|
+
const data = parseResult(res);
|
|
202
|
+
expect(data.status).toBe('dry_run');
|
|
203
|
+
expect(data.source_id).toBe(42);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// 11. confirm:true → deletion + audit
|
|
207
|
+
it('discards staging draft when confirm=true', async () => {
|
|
208
|
+
mockSuccess(mockStagingDraft());
|
|
209
|
+
mockSuccess({ deleted: true });
|
|
210
|
+
|
|
211
|
+
const res = await call('wp_discard_staging_draft', { staging_id: 99, confirm: true });
|
|
212
|
+
const data = parseResult(res);
|
|
213
|
+
expect(data.status).toBe('discarded');
|
|
214
|
+
expect(data.staging_id).toBe(99);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
// =========================================================================
|
|
219
|
+
// 12-14. WP_VISUAL_STAGING interception
|
|
220
|
+
// =========================================================================
|
|
221
|
+
describe('WP_VISUAL_STAGING interception', () => {
|
|
222
|
+
// 12. wp_update_post on published → intercepted
|
|
223
|
+
it('intercepts wp_update_post on published content when WP_VISUAL_STAGING=true', async () => {
|
|
224
|
+
process.env.WP_VISUAL_STAGING = 'true';
|
|
225
|
+
// GET /posts/42 → published post
|
|
226
|
+
mockSuccess({ id: 42, status: 'publish', title: { rendered: 'Test' }, content: { rendered: '' }, meta: {} });
|
|
227
|
+
|
|
228
|
+
const res = await call('wp_update_post', { id: 42, title: 'New Title' });
|
|
229
|
+
const data = parseResult(res);
|
|
230
|
+
expect(data.status).toBe('intercepted');
|
|
231
|
+
expect(data.reason).toContain('WP_VISUAL_STAGING');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// 13. wp_update_post on draft → NOT intercepted (normal flow)
|
|
235
|
+
it('does NOT intercept wp_update_post on draft content', async () => {
|
|
236
|
+
process.env.WP_VISUAL_STAGING = 'true';
|
|
237
|
+
// GET /posts/42 → draft post
|
|
238
|
+
mockSuccess({ id: 42, status: 'draft', title: { rendered: 'Test' }, content: { rendered: '' }, meta: {} });
|
|
239
|
+
// POST /posts/42 → updated
|
|
240
|
+
mockSuccess({ id: 42, status: 'draft', title: { rendered: 'New Title' }, link: 'https://example.com/?p=42', modified: '2025-06-16' });
|
|
241
|
+
|
|
242
|
+
const res = await call('wp_update_post', { id: 42, title: 'New Title' });
|
|
243
|
+
const data = parseResult(res);
|
|
244
|
+
expect(data.success).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
// 14. WP_VISUAL_STAGING=false → normal (no interception)
|
|
248
|
+
it('does NOT intercept when WP_VISUAL_STAGING is not set', async () => {
|
|
249
|
+
// No WP_VISUAL_STAGING env var
|
|
250
|
+
// POST /posts/42 → updated
|
|
251
|
+
mockSuccess({ id: 42, status: 'publish', title: { rendered: 'New Title' }, link: 'https://example.com/test/', modified: '2025-06-16' });
|
|
252
|
+
|
|
253
|
+
const res = await call('wp_update_post', { id: 42, title: 'New Title' });
|
|
254
|
+
const data = parseResult(res);
|
|
255
|
+
expect(data.success).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// =========================================================================
|
|
260
|
+
// 15. TOOLS_DEFINITIONS contains the 5 new tools
|
|
261
|
+
// =========================================================================
|
|
262
|
+
describe('TOOLS_DEFINITIONS', () => {
|
|
263
|
+
it('contains the 5 visual staging tools with workflow category', () => {
|
|
264
|
+
const stagingTools = ['wp_create_staging_draft', 'wp_list_staging_drafts', 'wp_get_staging_preview_url', 'wp_merge_staging_to_live', 'wp_discard_staging_draft'];
|
|
265
|
+
for (const toolName of stagingTools) {
|
|
266
|
+
const tool = TOOLS_DEFINITIONS.find(t => t.name === toolName);
|
|
267
|
+
expect(tool, `${toolName} should exist in TOOLS_DEFINITIONS`).toBeDefined();
|
|
268
|
+
expect(tool._category).toBe('workflow');
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|