@adsim/wordpress-mcp-server 1.0.0 → 3.0.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.
Files changed (64) hide show
  1. package/.env.example +8 -0
  2. package/.github/workflows/ci.yml +20 -0
  3. package/LICENSE +1 -1
  4. package/README.md +596 -135
  5. package/index.js +1367 -0
  6. package/package.json +21 -33
  7. package/src/auth/bearer.js +72 -0
  8. package/src/transport/http.js +264 -0
  9. package/tests/helpers/mockWpRequest.js +135 -0
  10. package/tests/unit/governance.test.js +260 -0
  11. package/tests/unit/tools/comments.test.js +170 -0
  12. package/tests/unit/tools/media.test.js +279 -0
  13. package/tests/unit/tools/pages.test.js +222 -0
  14. package/tests/unit/tools/plugins.test.js +268 -0
  15. package/tests/unit/tools/posts.test.js +310 -0
  16. package/tests/unit/tools/revisions.test.js +299 -0
  17. package/tests/unit/tools/search.test.js +190 -0
  18. package/tests/unit/tools/seo.test.js +248 -0
  19. package/tests/unit/tools/site.test.js +133 -0
  20. package/tests/unit/tools/taxonomies.test.js +220 -0
  21. package/tests/unit/tools/themes.test.js +163 -0
  22. package/tests/unit/tools/users.test.js +113 -0
  23. package/tests/unit/transport/http.test.js +300 -0
  24. package/vitest.config.js +12 -0
  25. package/dist/constants.d.ts +0 -13
  26. package/dist/constants.d.ts.map +0 -1
  27. package/dist/constants.js +0 -10
  28. package/dist/constants.js.map +0 -1
  29. package/dist/index.d.ts +0 -3
  30. package/dist/index.d.ts.map +0 -1
  31. package/dist/index.js +0 -33
  32. package/dist/index.js.map +0 -1
  33. package/dist/schemas/index.d.ts +0 -308
  34. package/dist/schemas/index.d.ts.map +0 -1
  35. package/dist/schemas/index.js +0 -191
  36. package/dist/schemas/index.js.map +0 -1
  37. package/dist/services/formatters.d.ts +0 -22
  38. package/dist/services/formatters.d.ts.map +0 -1
  39. package/dist/services/formatters.js +0 -52
  40. package/dist/services/formatters.js.map +0 -1
  41. package/dist/services/wp-client.d.ts +0 -38
  42. package/dist/services/wp-client.d.ts.map +0 -1
  43. package/dist/services/wp-client.js +0 -102
  44. package/dist/services/wp-client.js.map +0 -1
  45. package/dist/tools/content.d.ts +0 -4
  46. package/dist/tools/content.d.ts.map +0 -1
  47. package/dist/tools/content.js +0 -196
  48. package/dist/tools/content.js.map +0 -1
  49. package/dist/tools/posts.d.ts +0 -4
  50. package/dist/tools/posts.d.ts.map +0 -1
  51. package/dist/tools/posts.js +0 -179
  52. package/dist/tools/posts.js.map +0 -1
  53. package/dist/tools/seo.d.ts +0 -4
  54. package/dist/tools/seo.d.ts.map +0 -1
  55. package/dist/tools/seo.js +0 -241
  56. package/dist/tools/seo.js.map +0 -1
  57. package/dist/tools/taxonomy.d.ts +0 -4
  58. package/dist/tools/taxonomy.d.ts.map +0 -1
  59. package/dist/tools/taxonomy.js +0 -82
  60. package/dist/tools/taxonomy.js.map +0 -1
  61. package/dist/types.d.ts +0 -160
  62. package/dist/types.d.ts.map +0 -1
  63. package/dist/types.js +0 -3
  64. package/dist/types.js.map +0 -1
@@ -0,0 +1,279 @@
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 } from '../../../index.js';
7
+ import { mockSuccess, mockError, getAuditLogs, makeRequest, parseResult } from '../../helpers/mockWpRequest.js';
8
+
9
+ function call(name, args = {}) {
10
+ return handleToolCall(makeRequest(name, args));
11
+ }
12
+
13
+ // ────────────────────────────────────────────────────────────
14
+ // wp_list_media
15
+ // ────────────────────────────────────────────────────────────
16
+
17
+ describe('wp_list_media', () => {
18
+ let consoleSpy;
19
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
20
+ afterEach(() => { consoleSpy.mockRestore(); });
21
+
22
+ it('returns formatted media list on success', async () => {
23
+ fetch.mockResolvedValue(mockSuccess([
24
+ { id: 100, title: { rendered: 'Image 1' }, date: '2024-01-01', mime_type: 'image/jpeg', source_url: 'https://test.example.com/wp-content/uploads/img.jpg', alt_text: 'Alt', media_details: { width: 800, height: 600 } }
25
+ ]));
26
+
27
+ const result = await call('wp_list_media');
28
+ const data = parseResult(result);
29
+
30
+ expect(data.total).toBe(1);
31
+ expect(data.page).toBe(1);
32
+ expect(data.media).toHaveLength(1);
33
+ expect(data.media[0].id).toBe(100);
34
+ expect(data.media[0].title).toBe('Image 1');
35
+ expect(data.media[0].mime_type).toBe('image/jpeg');
36
+ expect(data.media[0].source_url).toBe('https://test.example.com/wp-content/uploads/img.jpg');
37
+ expect(data.media[0].alt_text).toBe('Alt');
38
+ expect(data.media[0].width).toBe(800);
39
+ expect(data.media[0].height).toBe(600);
40
+ });
41
+
42
+ it('returns error on 403', async () => {
43
+ fetch.mockResolvedValue(mockError(403));
44
+
45
+ const result = await call('wp_list_media');
46
+
47
+ expect(result.isError).toBe(true);
48
+ });
49
+
50
+ it('returns error on 404', async () => {
51
+ fetch.mockResolvedValue(mockError(404));
52
+
53
+ const result = await call('wp_list_media');
54
+
55
+ expect(result.isError).toBe(true);
56
+ });
57
+
58
+ it('logs audit entry with status success', async () => {
59
+ fetch.mockResolvedValue(mockSuccess([
60
+ { id: 100, title: { rendered: 'Image 1' }, date: '2024-01-01', mime_type: 'image/jpeg', source_url: 'https://test.example.com/wp-content/uploads/img.jpg', alt_text: 'Alt', media_details: { width: 800, height: 600 } }
61
+ ]));
62
+
63
+ await call('wp_list_media');
64
+ const logs = getAuditLogs(consoleSpy);
65
+ const entry = logs.find(l => l.tool === 'wp_list_media');
66
+
67
+ expect(entry).toBeDefined();
68
+ expect(entry.status).toBe('success');
69
+ });
70
+ });
71
+
72
+ // ────────────────────────────────────────────────────────────
73
+ // wp_get_media
74
+ // ────────────────────────────────────────────────────────────
75
+
76
+ describe('wp_get_media', () => {
77
+ let consoleSpy;
78
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
79
+ afterEach(() => { consoleSpy.mockRestore(); });
80
+
81
+ it('returns full media details on success', async () => {
82
+ fetch.mockResolvedValue(mockSuccess({
83
+ id: 100, title: { rendered: 'Image 1' }, date: '2024-01-01', mime_type: 'image/jpeg',
84
+ source_url: 'https://test.example.com/wp-content/uploads/img.jpg', alt_text: 'Alt',
85
+ caption: { rendered: 'Caption' },
86
+ media_details: {
87
+ width: 800, height: 600, file: '2024/01/img.jpg',
88
+ sizes: {
89
+ thumbnail: { source_url: 'https://test.example.com/wp-content/uploads/img-150x150.jpg', width: 150, height: 150 }
90
+ }
91
+ }
92
+ }));
93
+
94
+ const result = await call('wp_get_media', { id: 100 });
95
+ const data = parseResult(result);
96
+
97
+ expect(data.id).toBe(100);
98
+ expect(data.title).toBe('Image 1');
99
+ expect(data.mime_type).toBe('image/jpeg');
100
+ expect(data.source_url).toBe('https://test.example.com/wp-content/uploads/img.jpg');
101
+ expect(data.alt_text).toBe('Alt');
102
+ expect(data.caption).toBe('Caption');
103
+ expect(data.width).toBe(800);
104
+ expect(data.height).toBe(600);
105
+ expect(data.file).toBe('2024/01/img.jpg');
106
+ expect(data.sizes.thumbnail).toBeDefined();
107
+ expect(data.sizes.thumbnail.url).toBe('https://test.example.com/wp-content/uploads/img-150x150.jpg');
108
+ expect(data.sizes.thumbnail.width).toBe(150);
109
+ expect(data.sizes.thumbnail.height).toBe(150);
110
+ });
111
+
112
+ it('returns error on 403', async () => {
113
+ fetch.mockResolvedValue(mockError(403));
114
+
115
+ const result = await call('wp_get_media', { id: 100 });
116
+
117
+ expect(result.isError).toBe(true);
118
+ });
119
+
120
+ it('returns error on 404', async () => {
121
+ fetch.mockResolvedValue(mockError(404));
122
+
123
+ const result = await call('wp_get_media', { id: 999 });
124
+
125
+ expect(result.isError).toBe(true);
126
+ });
127
+
128
+ it('logs audit entry with status success', async () => {
129
+ fetch.mockResolvedValue(mockSuccess({
130
+ id: 100, title: { rendered: 'Image 1' }, date: '2024-01-01', mime_type: 'image/jpeg',
131
+ source_url: 'https://test.example.com/wp-content/uploads/img.jpg', alt_text: 'Alt',
132
+ caption: { rendered: 'Caption' },
133
+ media_details: { width: 800, height: 600, file: '2024/01/img.jpg', sizes: {} }
134
+ }));
135
+
136
+ await call('wp_get_media', { id: 100 });
137
+ const logs = getAuditLogs(consoleSpy);
138
+ const entry = logs.find(l => l.tool === 'wp_get_media');
139
+
140
+ expect(entry).toBeDefined();
141
+ expect(entry.status).toBe('success');
142
+ expect(entry.target).toBe(100);
143
+ });
144
+ });
145
+
146
+ // ────────────────────────────────────────────────────────────
147
+ // wp_upload_media
148
+ // ────────────────────────────────────────────────────────────
149
+
150
+ describe('wp_upload_media', () => {
151
+ let consoleSpy;
152
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
153
+ afterEach(() => { consoleSpy.mockRestore(); });
154
+
155
+ it('downloads file and uploads to WP on success', async () => {
156
+ let callCount = 0;
157
+ fetch.mockImplementation(() => {
158
+ callCount++;
159
+ if (callCount === 1) {
160
+ // First call: download the remote file
161
+ return Promise.resolve({
162
+ ok: true,
163
+ status: 200,
164
+ headers: { get: (name) => name === 'content-type' ? 'image/jpeg' : null },
165
+ buffer: () => Promise.resolve(Buffer.from('fake-image-data')),
166
+ json: () => Promise.resolve({}),
167
+ text: () => Promise.resolve('')
168
+ });
169
+ }
170
+ // Second call: upload to WP API
171
+ return Promise.resolve(mockSuccess({
172
+ id: 101,
173
+ source_url: 'https://test.example.com/wp-content/uploads/uploaded.jpg',
174
+ mime_type: 'image/jpeg'
175
+ }));
176
+ });
177
+
178
+ const result = await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
179
+ const data = parseResult(result);
180
+
181
+ expect(data.success).toBe(true);
182
+ expect(data.media.id).toBe(101);
183
+ expect(data.media.source_url).toBe('https://test.example.com/wp-content/uploads/uploaded.jpg');
184
+ expect(data.media.mime_type).toBe('image/jpeg');
185
+ });
186
+
187
+ it('is blocked in READ-ONLY mode (governance)', async () => {
188
+ process.env.WP_READ_ONLY = 'true';
189
+
190
+ const result = await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
191
+
192
+ expect(result.isError).toBe(true);
193
+ expect(result.content[0].text).toContain('READ-ONLY');
194
+
195
+ delete process.env.WP_READ_ONLY;
196
+ });
197
+
198
+ it('returns error when download fails with 403', async () => {
199
+ fetch.mockImplementation(() => {
200
+ return Promise.resolve({
201
+ ok: false,
202
+ status: 403,
203
+ headers: { get: () => null },
204
+ buffer: () => Promise.resolve(Buffer.from('')),
205
+ json: () => Promise.resolve({ message: 'Forbidden' }),
206
+ text: () => Promise.resolve('Forbidden')
207
+ });
208
+ });
209
+
210
+ const result = await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
211
+
212
+ expect(result.isError).toBe(true);
213
+ });
214
+
215
+ it('returns error when WP upload API returns 403', async () => {
216
+ let callCount = 0;
217
+ fetch.mockImplementation(() => {
218
+ callCount++;
219
+ if (callCount === 1) {
220
+ return Promise.resolve({
221
+ ok: true,
222
+ status: 200,
223
+ headers: { get: (name) => name === 'content-type' ? 'image/jpeg' : null },
224
+ buffer: () => Promise.resolve(Buffer.from('fake-image-data')),
225
+ json: () => Promise.resolve({}),
226
+ text: () => Promise.resolve('')
227
+ });
228
+ }
229
+ return Promise.resolve(mockError(403));
230
+ });
231
+
232
+ const result = await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
233
+
234
+ expect(result.isError).toBe(true);
235
+ });
236
+
237
+ it('logs audit entry with status success on upload', async () => {
238
+ let callCount = 0;
239
+ fetch.mockImplementation(() => {
240
+ callCount++;
241
+ if (callCount === 1) {
242
+ return Promise.resolve({
243
+ ok: true,
244
+ status: 200,
245
+ headers: { get: (name) => name === 'content-type' ? 'image/jpeg' : null },
246
+ buffer: () => Promise.resolve(Buffer.from('fake-image-data')),
247
+ json: () => Promise.resolve({}),
248
+ text: () => Promise.resolve('')
249
+ });
250
+ }
251
+ return Promise.resolve(mockSuccess({
252
+ id: 101,
253
+ source_url: 'https://test.example.com/wp-content/uploads/uploaded.jpg',
254
+ mime_type: 'image/jpeg'
255
+ }));
256
+ });
257
+
258
+ await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
259
+ const logs = getAuditLogs(consoleSpy);
260
+ const entry = logs.find(l => l.tool === 'wp_upload_media');
261
+
262
+ expect(entry).toBeDefined();
263
+ expect(entry.status).toBe('success');
264
+ expect(entry.target).toBe(101);
265
+ });
266
+
267
+ it('logs audit entry with status blocked in READ-ONLY mode', async () => {
268
+ process.env.WP_READ_ONLY = 'true';
269
+
270
+ await call('wp_upload_media', { url: 'https://remote.example.com/photo.jpg' });
271
+ const logs = getAuditLogs(consoleSpy);
272
+ const entry = logs.find(l => l.tool === 'wp_upload_media');
273
+
274
+ expect(entry).toBeDefined();
275
+ expect(entry.status).toBe('blocked');
276
+
277
+ delete process.env.WP_READ_ONLY;
278
+ });
279
+ });
@@ -0,0 +1,222 @@
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 } from '../../../index.js';
7
+ import { mockSuccess, mockError, getAuditLogs, makeRequest, parseResult } from '../../helpers/mockWpRequest.js';
8
+
9
+ function call(name, args = {}) {
10
+ return handleToolCall(makeRequest(name, args));
11
+ }
12
+
13
+ // ────────────────────────────────────────────────────────────
14
+ // wp_list_pages
15
+ // ────────────────────────────────────────────────────────────
16
+
17
+ describe('wp_list_pages', () => {
18
+ let consoleSpy;
19
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
20
+ afterEach(() => { consoleSpy.mockRestore(); });
21
+
22
+ it('returns formatted page list on success', async () => {
23
+ fetch.mockResolvedValue(mockSuccess([
24
+ { id: 10, title: { rendered: 'About' }, status: 'publish', date: '2024-01-01', link: 'https://test.example.com/about', parent: 0, menu_order: 1, template: '', excerpt: { rendered: 'About page' } }
25
+ ]));
26
+
27
+ const result = await call('wp_list_pages');
28
+ const data = parseResult(result);
29
+
30
+ expect(data.total).toBe(1);
31
+ expect(data.pages[0].id).toBe(10);
32
+ expect(data.pages[0].title).toBe('About');
33
+ expect(data.pages[0].parent).toBe(0);
34
+ expect(data.pages[0].menu_order).toBe(1);
35
+ });
36
+
37
+ it('logs audit entry with status success', async () => {
38
+ fetch.mockResolvedValue(mockSuccess([
39
+ { id: 10, title: { rendered: 'About' }, status: 'publish', date: '2024-01-01', link: 'https://test.example.com/about', parent: 0, menu_order: 1, template: '', excerpt: { rendered: 'About page' } }
40
+ ]));
41
+
42
+ await call('wp_list_pages');
43
+ const logs = getAuditLogs(consoleSpy);
44
+ const entry = logs.find(l => l.tool === 'wp_list_pages');
45
+
46
+ expect(entry).toBeDefined();
47
+ expect(entry.status).toBe('success');
48
+ expect(entry.action).toBe('list');
49
+ });
50
+ });
51
+
52
+ // ────────────────────────────────────────────────────────────
53
+ // wp_get_page
54
+ // ────────────────────────────────────────────────────────────
55
+
56
+ describe('wp_get_page', () => {
57
+ let consoleSpy;
58
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
59
+ afterEach(() => { consoleSpy.mockRestore(); });
60
+
61
+ it('returns full page data on success', async () => {
62
+ fetch.mockResolvedValue(mockSuccess({
63
+ id: 10, title: { rendered: 'About' }, content: { rendered: '<p>About us</p>' }, excerpt: { rendered: 'About page' },
64
+ status: 'publish', date: '2024-01-01', modified: '2024-01-01', link: 'https://test.example.com/about',
65
+ slug: 'about', parent: 0, menu_order: 1, template: '', author: 1, featured_media: 0, meta: {}
66
+ }));
67
+
68
+ const result = await call('wp_get_page', { id: 10 });
69
+ const data = parseResult(result);
70
+
71
+ expect(data.id).toBe(10);
72
+ expect(data.title).toBe('About');
73
+ expect(data.content).toBe('<p>About us</p>');
74
+ expect(data.slug).toBe('about');
75
+ expect(data.parent).toBe(0);
76
+ expect(data.template).toBe('');
77
+ });
78
+
79
+ it('returns error on 404', async () => {
80
+ fetch.mockResolvedValue(mockError(404));
81
+
82
+ const result = await call('wp_get_page', { id: 999 });
83
+
84
+ expect(result.isError).toBe(true);
85
+ expect(result.content[0].text).toContain('404');
86
+ });
87
+
88
+ it('logs audit with target and target_type', async () => {
89
+ fetch.mockResolvedValue(mockSuccess({
90
+ id: 10, title: { rendered: 'About' }, content: { rendered: '<p>About us</p>' }, excerpt: { rendered: 'About page' },
91
+ status: 'publish', date: '2024-01-01', modified: '2024-01-01', link: 'https://test.example.com/about',
92
+ slug: 'about', parent: 0, menu_order: 1, template: '', author: 1, featured_media: 0, meta: {}
93
+ }));
94
+
95
+ await call('wp_get_page', { id: 10 });
96
+ const logs = getAuditLogs(consoleSpy);
97
+ const entry = logs.find(l => l.tool === 'wp_get_page');
98
+
99
+ expect(entry).toBeDefined();
100
+ expect(entry.status).toBe('success');
101
+ expect(entry.target).toBe(10);
102
+ expect(entry.target_type).toBe('page');
103
+ });
104
+ });
105
+
106
+ // ────────────────────────────────────────────────────────────
107
+ // wp_create_page
108
+ // ────────────────────────────────────────────────────────────
109
+
110
+ describe('wp_create_page', () => {
111
+ let consoleSpy;
112
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
113
+ afterEach(() => { consoleSpy.mockRestore(); });
114
+
115
+ it('creates a page and returns success shape', async () => {
116
+ fetch.mockResolvedValue(mockSuccess({
117
+ id: 11, title: { rendered: 'New Page' }, status: 'draft', link: 'https://test.example.com/?page_id=11', parent: 0
118
+ }));
119
+
120
+ const result = await call('wp_create_page', { title: 'New Page', content: 'Page body' });
121
+ const data = parseResult(result);
122
+
123
+ expect(data.success).toBe(true);
124
+ expect(data.page.id).toBe(11);
125
+ expect(data.page.title).toBe('New Page');
126
+ expect(data.page.status).toBe('draft');
127
+ expect(data.page.parent).toBe(0);
128
+ });
129
+
130
+ it('is blocked by WP_READ_ONLY', async () => {
131
+ process.env.WP_READ_ONLY = 'true';
132
+ try {
133
+ const result = await call('wp_create_page', { title: 'T', content: 'C' });
134
+
135
+ expect(result.isError).toBe(true);
136
+ expect(result.content[0].text).toContain('READ-ONLY');
137
+
138
+ const logs = getAuditLogs(consoleSpy);
139
+ const entry = logs.find(l => l.tool === 'wp_create_page');
140
+ expect(entry).toBeDefined();
141
+ expect(entry.status).toBe('blocked');
142
+ } finally {
143
+ delete process.env.WP_READ_ONLY;
144
+ }
145
+ });
146
+
147
+ it('logs audit on success', async () => {
148
+ fetch.mockResolvedValue(mockSuccess({
149
+ id: 11, title: { rendered: 'New Page' }, status: 'draft', link: 'https://test.example.com/?page_id=11', parent: 0
150
+ }));
151
+
152
+ await call('wp_create_page', { title: 'New Page', content: 'Body' });
153
+ const logs = getAuditLogs(consoleSpy);
154
+ const entry = logs.find(l => l.tool === 'wp_create_page');
155
+
156
+ expect(entry).toBeDefined();
157
+ expect(entry.status).toBe('success');
158
+ expect(entry.action).toBe('create');
159
+ expect(entry.target_type).toBe('page');
160
+ });
161
+ });
162
+
163
+ // ────────────────────────────────────────────────────────────
164
+ // wp_update_page
165
+ // ────────────────────────────────────────────────────────────
166
+
167
+ describe('wp_update_page', () => {
168
+ let consoleSpy;
169
+ beforeEach(() => { fetch.mockReset(); consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); });
170
+ afterEach(() => { consoleSpy.mockRestore(); });
171
+
172
+ it('updates a page and returns success shape', async () => {
173
+ fetch.mockResolvedValue(mockSuccess({
174
+ id: 10, title: { rendered: 'Updated' }, status: 'publish', link: 'https://test.example.com/about', modified: '2024-01-02'
175
+ }));
176
+
177
+ const result = await call('wp_update_page', { id: 10, title: 'Updated' });
178
+ const data = parseResult(result);
179
+
180
+ expect(data.success).toBe(true);
181
+ expect(data.page.id).toBe(10);
182
+ expect(data.page.title).toBe('Updated');
183
+ expect(data.page.modified).toBe('2024-01-02');
184
+ });
185
+
186
+ it('returns error on 404', async () => {
187
+ fetch.mockResolvedValue(mockError(404));
188
+
189
+ const result = await call('wp_update_page', { id: 999, title: 'Ghost' });
190
+
191
+ expect(result.isError).toBe(true);
192
+ expect(result.content[0].text).toContain('404');
193
+ });
194
+
195
+ it('is blocked by WP_READ_ONLY', async () => {
196
+ process.env.WP_READ_ONLY = 'true';
197
+ try {
198
+ const result = await call('wp_update_page', { id: 10, title: 'T' });
199
+
200
+ expect(result.isError).toBe(true);
201
+ expect(result.content[0].text).toContain('READ-ONLY');
202
+ } finally {
203
+ delete process.env.WP_READ_ONLY;
204
+ }
205
+ });
206
+
207
+ it('logs audit with target and action', async () => {
208
+ fetch.mockResolvedValue(mockSuccess({
209
+ id: 10, title: { rendered: 'Updated' }, status: 'publish', link: 'https://test.example.com/about', modified: '2024-01-02'
210
+ }));
211
+
212
+ await call('wp_update_page', { id: 10, title: 'Updated' });
213
+ const logs = getAuditLogs(consoleSpy);
214
+ const entry = logs.find(l => l.tool === 'wp_update_page');
215
+
216
+ expect(entry).toBeDefined();
217
+ expect(entry.status).toBe('success');
218
+ expect(entry.action).toBe('update');
219
+ expect(entry.target).toBe(10);
220
+ expect(entry.target_type).toBe('page');
221
+ });
222
+ });