@gravitykit/block-mcp 2.0.0-beta
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 +15 -0
- package/LICENSE +26 -0
- package/README.md +592 -0
- package/dist/index.cjs +52721 -0
- package/package.json +70 -0
- package/src/__tests__/fixtures/block-trees.ts +199 -0
- package/src/__tests__/fixtures/error-envelopes.ts +115 -0
- package/src/__tests__/fixtures/rest-responses.ts +280 -0
- package/src/__tests__/helpers/mock-client.ts +185 -0
- package/src/__tests__/helpers/request-matchers.ts +88 -0
- package/src/__tests__/helpers/schema-asserts.ts +132 -0
- package/src/__tests__/integration/concurrency.test.ts +129 -0
- package/src/__tests__/integration/dual-storage.test.ts +156 -0
- package/src/__tests__/integration/error-envelopes.test.ts +238 -0
- package/src/__tests__/integration/global-setup.ts +17 -0
- package/src/__tests__/integration/rate-limit.test.ts +88 -0
- package/src/__tests__/integration/read-edit-read.test.ts +141 -0
- package/src/__tests__/integration/ref-stability.test.ts +175 -0
- package/src/__tests__/integration/setup.ts +201 -0
- package/src/__tests__/tools/discovery/get_pattern.test.ts +58 -0
- package/src/__tests__/tools/discovery/get_post_info.test.ts +100 -0
- package/src/__tests__/tools/discovery/get_site_usage.test.ts +41 -0
- package/src/__tests__/tools/discovery/list_block_types.test.ts +103 -0
- package/src/__tests__/tools/discovery/list_patterns.test.ts +106 -0
- package/src/__tests__/tools/discovery/list_posts.test.ts +47 -0
- package/src/__tests__/tools/discovery/resolve_url.test.ts +69 -0
- package/src/__tests__/tools/discovery/scan_storage_modes.test.ts +34 -0
- package/src/__tests__/tools/media/upload_media.test.ts +123 -0
- package/src/__tests__/tools/mutate/edit_block_tree.test.ts +439 -0
- package/src/__tests__/tools/mutate/ref_routing.test.ts +105 -0
- package/src/__tests__/tools/patterns/insert_pattern.test.ts +117 -0
- package/src/__tests__/tools/posts/create_post.test.ts +84 -0
- package/src/__tests__/tools/posts/update_post.test.ts +93 -0
- package/src/__tests__/tools/read/get_block.test.ts +96 -0
- package/src/__tests__/tools/read/get_page_blocks.test.ts +184 -0
- package/src/__tests__/tools/read/persist_refs.test.ts +35 -0
- package/src/__tests__/tools/terms/list_terms.test.ts +91 -0
- package/src/__tests__/tools/write/delete_block.test.ts +91 -0
- package/src/__tests__/tools/write/insert_blocks.test.ts +149 -0
- package/src/__tests__/tools/write/ref_routing.test.ts +177 -0
- package/src/__tests__/tools/write/replace_block_range.test.ts +90 -0
- package/src/__tests__/tools/write/rewrite_post_blocks.test.ts +126 -0
- package/src/__tests__/tools/write/update_block.test.ts +206 -0
- package/src/__tests__/tools/write/update_blocks.test.ts +173 -0
- package/src/__tests__/tools/yoast/yoast_bulk_update_seo.test.ts +112 -0
- package/src/__tests__/tools/yoast/yoast_get_seo.test.ts +78 -0
- package/src/__tests__/tools/yoast/yoast_update_seo.test.ts +105 -0
- package/src/__tests__/unit/client/ref-endpoints.test.ts +232 -0
- package/src/__tests__/unit/enrichers/cbp-enricher.test.ts +457 -0
- package/src/__tests__/unit/error-translator/translate-wp-error.test.ts +318 -0
- package/src/__tests__/unit/instructions.test.ts +374 -0
- package/src/__tests__/unit/preferences/enrich-block-list.test.ts +175 -0
- package/src/__tests__/unit/preferences/enrich-pattern-list.test.ts +227 -0
- package/src/client.ts +964 -0
- package/src/connect.ts +877 -0
- package/src/enrichers.ts +348 -0
- package/src/error-translator.ts +156 -0
- package/src/index.ts +450 -0
- package/src/instructions.ts +270 -0
- package/src/preferences.ts +273 -0
- package/src/tools/discovery.ts +251 -0
- package/src/tools/media.ts +75 -0
- package/src/tools/mutate.ts +243 -0
- package/src/tools/patterns.ts +94 -0
- package/src/tools/posts.ts +200 -0
- package/src/tools/read.ts +201 -0
- package/src/tools/terms.ts +44 -0
- package/src/tools/write.ts +542 -0
- package/src/tools/yoast.ts +224 -0
- package/src/types.ts +862 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests: update_block
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Input validation (post_id, flat_index/ref XOR, attributes/innerHTML)
|
|
6
|
+
* - Index path → client.updateBlock
|
|
7
|
+
* - Ref path → client.updateBlockByRef
|
|
8
|
+
* - Enricher wiring (block_name triggers enrichBlock)
|
|
9
|
+
* - Response shape (success, saved snapshot, revision IDs)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
13
|
+
import { handleWriteTool } from '../../../tools/write.js';
|
|
14
|
+
import { makeMockClient } from '../../helpers/mock-client.js';
|
|
15
|
+
import { assertBlockUpdateResponse } from '../../helpers/schema-asserts.js';
|
|
16
|
+
|
|
17
|
+
vi.mock('../../../enrichers.js', () => ({
|
|
18
|
+
enrichBlock: vi.fn(async (block: any) => ({
|
|
19
|
+
...block,
|
|
20
|
+
attributes: { ...block.attributes, enriched: true },
|
|
21
|
+
})),
|
|
22
|
+
enrichBlocks: vi.fn(async (blocks: any[]) =>
|
|
23
|
+
blocks.map((b: any) => ({ ...b, attributes: { ...b.attributes, enriched: true } }))
|
|
24
|
+
),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
// ── Fixtures ──────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
const UPDATE_RESPONSE = {
|
|
30
|
+
success: true,
|
|
31
|
+
block: { index: 1, name: 'core/heading', attributes: { level: 3 }, ref: 'blk_head0001' },
|
|
32
|
+
saved: {
|
|
33
|
+
flat_index: 1, block_name: 'core/heading',
|
|
34
|
+
attributes: { level: 3 }, inner_html: '<h3>Title</h3>', is_dynamic: false, ref: 'blk_head0001',
|
|
35
|
+
},
|
|
36
|
+
before_revision_id: 100,
|
|
37
|
+
revision_id: 101,
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// ── Tests ─────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
describe('update_block — validation', () => {
|
|
43
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
44
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
45
|
+
|
|
46
|
+
it('requires post_id', async () => {
|
|
47
|
+
await expect(handleWriteTool('update_block', { flat_index: 0, attributes: {} }, client as any))
|
|
48
|
+
.rejects.toThrow('post_id');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('requires flat_index OR ref (not neither)', async () => {
|
|
52
|
+
await expect(handleWriteTool('update_block', { post_id: 1, attributes: {} }, client as any))
|
|
53
|
+
.rejects.toThrow(/Provide either flat_index/);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('rejects when both flat_index and ref provided', async () => {
|
|
57
|
+
await expect(
|
|
58
|
+
handleWriteTool('update_block', { post_id: 1, flat_index: 0, ref: 'blk_x', attributes: {} }, client as any)
|
|
59
|
+
).rejects.toThrow(/flat_index OR ref, not both/);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('rejects empty-string ref (treats as absent)', async () => {
|
|
63
|
+
await expect(
|
|
64
|
+
handleWriteTool('update_block', { post_id: 1, ref: '', attributes: {} }, client as any)
|
|
65
|
+
).rejects.toThrow(/Provide either flat_index/);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('requires attributes or innerHTML', async () => {
|
|
69
|
+
await expect(handleWriteTool('update_block', { post_id: 1, flat_index: 0 }, client as any))
|
|
70
|
+
.rejects.toThrow(/attributes or innerHTML/);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('rejects negative flat_index', async () => {
|
|
74
|
+
await expect(
|
|
75
|
+
handleWriteTool('update_block', { post_id: 1, flat_index: -1, attributes: {} }, client as any)
|
|
76
|
+
).rejects.toThrow(/Provide either flat_index/);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('rejects NaN flat_index', async () => {
|
|
80
|
+
await expect(
|
|
81
|
+
handleWriteTool('update_block', { post_id: 1, flat_index: NaN, attributes: {} }, client as any)
|
|
82
|
+
).rejects.toThrow(/Provide either flat_index/);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('update_block — index path', () => {
|
|
87
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
88
|
+
beforeEach(() => { client = makeMockClient(); client.updateBlock.mockResolvedValue(UPDATE_RESPONSE); vi.clearAllMocks(); });
|
|
89
|
+
|
|
90
|
+
it('routes to updateBlock (not updateBlockByRef)', async () => {
|
|
91
|
+
await handleWriteTool('update_block', { post_id: 1, flat_index: 5, attributes: { level: 3 } }, client as any);
|
|
92
|
+
expect(client.updateBlock).toHaveBeenCalledWith(1, 5, { attributes: { level: 3 }, innerHTML: undefined });
|
|
93
|
+
expect(client.updateBlockByRef).not.toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('passes innerHTML through index path', async () => {
|
|
97
|
+
await handleWriteTool('update_block', { post_id: 1, flat_index: 2, innerHTML: '<p>Hi</p>' }, client as any);
|
|
98
|
+
expect(client.updateBlock).toHaveBeenCalledWith(1, 2, { attributes: undefined, innerHTML: '<p>Hi</p>' });
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('passes both attributes and innerHTML', async () => {
|
|
102
|
+
await handleWriteTool('update_block', {
|
|
103
|
+
post_id: 1, flat_index: 0, attributes: { level: 2 }, innerHTML: '<h2>Title</h2>'
|
|
104
|
+
}, client as any);
|
|
105
|
+
expect(client.updateBlock).toHaveBeenCalledWith(1, 0, { attributes: { level: 2 }, innerHTML: '<h2>Title</h2>' });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('flat_index=0 is a valid target', async () => {
|
|
109
|
+
await handleWriteTool('update_block', { post_id: 1, flat_index: 0, attributes: { level: 1 } }, client as any);
|
|
110
|
+
expect(client.updateBlock).toHaveBeenCalledWith(1, 0, expect.any(Object));
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('update_block — ref path', () => {
|
|
115
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
116
|
+
beforeEach(() => { client = makeMockClient(); client.updateBlockByRef.mockResolvedValue(UPDATE_RESPONSE); vi.clearAllMocks(); });
|
|
117
|
+
|
|
118
|
+
it('routes to updateBlockByRef (not updateBlock)', async () => {
|
|
119
|
+
await handleWriteTool('update_block', { post_id: 1, ref: 'blk_abc12345', attributes: { level: 3 } }, client as any);
|
|
120
|
+
expect(client.updateBlockByRef).toHaveBeenCalledWith(1, 'blk_abc12345', { attributes: { level: 3 }, innerHTML: undefined });
|
|
121
|
+
expect(client.updateBlock).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('passes innerHTML through ref path', async () => {
|
|
125
|
+
await handleWriteTool('update_block', { post_id: 1, ref: 'blk_x', innerHTML: '<h2>hi</h2>' }, client as any);
|
|
126
|
+
expect(client.updateBlockByRef).toHaveBeenCalledWith(1, 'blk_x', { attributes: undefined, innerHTML: '<h2>hi</h2>' });
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
describe('update_block — enricher wiring', () => {
|
|
131
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
132
|
+
beforeEach(() => { client = makeMockClient(); client.updateBlock.mockResolvedValue(UPDATE_RESPONSE); vi.clearAllMocks(); });
|
|
133
|
+
|
|
134
|
+
it('skips enricher when block_name is absent', async () => {
|
|
135
|
+
const enrichers = await import('../../../enrichers.js');
|
|
136
|
+
const localEnrichBlock = enrichers.enrichBlock as ReturnType<typeof vi.fn>;
|
|
137
|
+
localEnrichBlock.mockClear();
|
|
138
|
+
await handleWriteTool('update_block', { post_id: 1, flat_index: 0, attributes: { level: 2 } }, client as any);
|
|
139
|
+
expect(localEnrichBlock).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('calls enrichBlock when block_name is provided', async () => {
|
|
143
|
+
const enrichers = await import('../../../enrichers.js');
|
|
144
|
+
const localEnrichBlock = enrichers.enrichBlock as ReturnType<typeof vi.fn>;
|
|
145
|
+
localEnrichBlock.mockClear();
|
|
146
|
+
await handleWriteTool('update_block', {
|
|
147
|
+
post_id: 1, flat_index: 0, block_name: 'core/heading', attributes: { level: 2 },
|
|
148
|
+
}, client as any);
|
|
149
|
+
expect(localEnrichBlock).toHaveBeenCalledWith({ name: 'core/heading', attributes: { level: 2 } });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('enricher-updated attributes reach the client', async () => {
|
|
153
|
+
await handleWriteTool('update_block', {
|
|
154
|
+
post_id: 1, flat_index: 0, block_name: 'core/heading', attributes: { level: 2 },
|
|
155
|
+
}, client as any);
|
|
156
|
+
const call = client.updateBlock.mock.calls[0] as unknown[];
|
|
157
|
+
const data = call[2] as { attributes: Record<string, unknown> };
|
|
158
|
+
expect(data.attributes).toMatchObject({ level: 2, enriched: true });
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('enricher can update innerHTML', async () => {
|
|
162
|
+
const enrichers = await import('../../../enrichers.js');
|
|
163
|
+
const localEnrichBlock = enrichers.enrichBlock as ReturnType<typeof vi.fn>;
|
|
164
|
+
localEnrichBlock.mockResolvedValueOnce({
|
|
165
|
+
name: 'core/heading',
|
|
166
|
+
attributes: { level: 2, enriched: true },
|
|
167
|
+
innerHTML: '<h2>Enriched</h2>',
|
|
168
|
+
});
|
|
169
|
+
await handleWriteTool('update_block', {
|
|
170
|
+
post_id: 1, flat_index: 0, block_name: 'core/heading',
|
|
171
|
+
attributes: { level: 2 }, innerHTML: '<h2>Original</h2>',
|
|
172
|
+
}, client as any);
|
|
173
|
+
expect(client.updateBlock).toHaveBeenCalledWith(1, 0, {
|
|
174
|
+
attributes: { level: 2, enriched: true }, innerHTML: '<h2>Enriched</h2>',
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('update_block — response shape', () => {
|
|
180
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
181
|
+
beforeEach(() => { client = makeMockClient(); client.updateBlock.mockResolvedValue(UPDATE_RESPONSE); vi.clearAllMocks(); });
|
|
182
|
+
|
|
183
|
+
it('returns a valid BlockUpdateResponse shape', async () => {
|
|
184
|
+
const result = await handleWriteTool('update_block', {
|
|
185
|
+
post_id: 1, flat_index: 1, attributes: { level: 3 }
|
|
186
|
+
}, client as any);
|
|
187
|
+
assertBlockUpdateResponse(result);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it('saved block matches expected shape', async () => {
|
|
191
|
+
const result = await handleWriteTool('update_block', {
|
|
192
|
+
post_id: 1, flat_index: 1, attributes: { level: 3 }
|
|
193
|
+
}, client as any) as any;
|
|
194
|
+
expect(result.saved.block_name).toBe('core/heading');
|
|
195
|
+
expect(result.saved.flat_index).toBe(1);
|
|
196
|
+
expect(result.saved.is_dynamic).toBe(false);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('revision IDs are present', async () => {
|
|
200
|
+
const result = await handleWriteTool('update_block', {
|
|
201
|
+
post_id: 1, flat_index: 1, attributes: { level: 3 }
|
|
202
|
+
}, client as any) as any;
|
|
203
|
+
expect(result.before_revision_id).toBe(100);
|
|
204
|
+
expect(result.revision_id).toBe(101);
|
|
205
|
+
});
|
|
206
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests: update_blocks (batch update)
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Input validation (post_id, non-empty updates, per-item ref/flat_index XOR)
|
|
6
|
+
* - Per-item payload validation (must have attributes or innerHTML)
|
|
7
|
+
* - Error message includes failing item index
|
|
8
|
+
* - Correct forwarding of normalized items to client.updateBlocksBatch
|
|
9
|
+
* - Enricher wiring for block_name items
|
|
10
|
+
* - verbose flag forwarding
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
14
|
+
import { handleWriteTool } from '../../../tools/write.js';
|
|
15
|
+
import { makeMockClient } from '../../helpers/mock-client.js';
|
|
16
|
+
|
|
17
|
+
vi.mock('../../../enrichers.js', () => ({
|
|
18
|
+
enrichBlock: vi.fn(async (block: any) => ({
|
|
19
|
+
...block,
|
|
20
|
+
attributes: { ...block.attributes, enriched: true },
|
|
21
|
+
})),
|
|
22
|
+
enrichBlocks: vi.fn(async (blocks: any[]) =>
|
|
23
|
+
blocks.map((b: any) => ({ ...b, attributes: { ...b.attributes, enriched: true } }))
|
|
24
|
+
),
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
describe('update_blocks — validation: post_id', () => {
|
|
28
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
29
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
30
|
+
|
|
31
|
+
it('requires post_id', async () => {
|
|
32
|
+
await expect(
|
|
33
|
+
handleWriteTool('update_blocks', { updates: [{ ref: 'blk_a', innerHTML: 'x' }] }, client as any)
|
|
34
|
+
).rejects.toThrow('post_id');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('update_blocks — validation: updates array', () => {
|
|
39
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
40
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
41
|
+
|
|
42
|
+
it('rejects empty updates array', async () => {
|
|
43
|
+
await expect(
|
|
44
|
+
handleWriteTool('update_blocks', { post_id: 1, updates: [] }, client as any)
|
|
45
|
+
).rejects.toThrow('non-empty');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('rejects missing updates field', async () => {
|
|
49
|
+
await expect(
|
|
50
|
+
handleWriteTool('update_blocks', { post_id: 1 }, client as any)
|
|
51
|
+
).rejects.toThrow('non-empty');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('update_blocks — per-item validation', () => {
|
|
56
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
57
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
58
|
+
|
|
59
|
+
it('rejects items missing both ref and flat_index', async () => {
|
|
60
|
+
await expect(
|
|
61
|
+
handleWriteTool('update_blocks', { post_id: 1, updates: [{ innerHTML: 'x' }] }, client as any)
|
|
62
|
+
).rejects.toThrow('exactly one of ref or flat_index');
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('rejects items with both ref and flat_index', async () => {
|
|
66
|
+
await expect(
|
|
67
|
+
handleWriteTool('update_blocks', {
|
|
68
|
+
post_id: 1, updates: [{ ref: 'blk_a', flat_index: 0, innerHTML: 'x' }],
|
|
69
|
+
}, client as any)
|
|
70
|
+
).rejects.toThrow('exactly one of ref or flat_index');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('rejects items missing payload (no attributes or innerHTML)', async () => {
|
|
74
|
+
await expect(
|
|
75
|
+
handleWriteTool('update_blocks', { post_id: 1, updates: [{ ref: 'blk_a' }] }, client as any)
|
|
76
|
+
).rejects.toThrow('attributes or innerHTML');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('error message includes the failing item index', async () => {
|
|
80
|
+
await expect(
|
|
81
|
+
handleWriteTool('update_blocks', {
|
|
82
|
+
post_id: 1,
|
|
83
|
+
updates: [
|
|
84
|
+
{ ref: 'blk_a', innerHTML: 'x' },
|
|
85
|
+
{ ref: 'blk_b' }, // missing payload at index 1
|
|
86
|
+
],
|
|
87
|
+
}, client as any)
|
|
88
|
+
).rejects.toThrow('updates[1]');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('error includes index for failing item deeper in the array', async () => {
|
|
92
|
+
await expect(
|
|
93
|
+
handleWriteTool('update_blocks', {
|
|
94
|
+
post_id: 1,
|
|
95
|
+
updates: [
|
|
96
|
+
{ ref: 'blk_a', innerHTML: 'x' },
|
|
97
|
+
{ ref: 'blk_b', attributes: { level: 2 } },
|
|
98
|
+
{ innerHTML: 'y' }, // missing ref/flat_index at index 2
|
|
99
|
+
],
|
|
100
|
+
}, client as any)
|
|
101
|
+
).rejects.toThrow('updates[2]');
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('update_blocks — forwarding to client', () => {
|
|
106
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
107
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
108
|
+
|
|
109
|
+
it('forwards normalized items to updateBlocksBatch', async () => {
|
|
110
|
+
await handleWriteTool('update_blocks', {
|
|
111
|
+
post_id: 42,
|
|
112
|
+
updates: [
|
|
113
|
+
{ ref: 'blk_a', innerHTML: '<p>One</p>' },
|
|
114
|
+
{ flat_index: 5, attributes: { level: 3 } },
|
|
115
|
+
],
|
|
116
|
+
}, client as any);
|
|
117
|
+
expect(client.updateBlocksBatch).toHaveBeenCalledWith(42, [
|
|
118
|
+
{ ref: 'blk_a', innerHTML: '<p>One</p>' },
|
|
119
|
+
{ flat_index: 5, attributes: { level: 3 } },
|
|
120
|
+
]);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('omits verbose option when flag is not set (two-arg call)', async () => {
|
|
124
|
+
await handleWriteTool('update_blocks', {
|
|
125
|
+
post_id: 7, updates: [{ ref: 'blk_x', attributes: { level: 2 } }],
|
|
126
|
+
}, client as any);
|
|
127
|
+
expect(client.updateBlocksBatch.mock.calls[0]).toHaveLength(2);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('forwards verbose:true when requested (three-arg call)', async () => {
|
|
131
|
+
await handleWriteTool('update_blocks', {
|
|
132
|
+
post_id: 7, updates: [{ ref: 'blk_x', attributes: { level: 2 } }], verbose: true,
|
|
133
|
+
}, client as any);
|
|
134
|
+
expect(client.updateBlocksBatch).toHaveBeenCalledWith(
|
|
135
|
+
7,
|
|
136
|
+
[{ ref: 'blk_x', attributes: { level: 2 } }],
|
|
137
|
+
{ verbose: true }
|
|
138
|
+
);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('treats non-boolean verbose as false (defensive)', async () => {
|
|
142
|
+
await handleWriteTool('update_blocks', {
|
|
143
|
+
post_id: 7, updates: [{ ref: 'blk_x', attributes: { level: 2 } }],
|
|
144
|
+
verbose: 'true' as unknown as boolean,
|
|
145
|
+
}, client as any);
|
|
146
|
+
expect(client.updateBlocksBatch.mock.calls[0]).toHaveLength(2);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('update_blocks — enricher wiring', () => {
|
|
151
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
152
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
153
|
+
|
|
154
|
+
it('enriches item attributes when block_name is supplied', async () => {
|
|
155
|
+
await handleWriteTool('update_blocks', {
|
|
156
|
+
post_id: 7,
|
|
157
|
+
updates: [{ ref: 'blk_x', block_name: 'core/heading', attributes: { level: 2 } }],
|
|
158
|
+
}, client as any);
|
|
159
|
+
const call = client.updateBlocksBatch.mock.calls[0] as unknown[];
|
|
160
|
+
const normalized = call[1] as Array<{ attributes: Record<string, unknown> }>;
|
|
161
|
+
expect(normalized[0].attributes).toEqual({ level: 2, enriched: true });
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('passes item through unchanged when block_name is absent', async () => {
|
|
165
|
+
await handleWriteTool('update_blocks', {
|
|
166
|
+
post_id: 7,
|
|
167
|
+
updates: [{ ref: 'blk_x', attributes: { level: 2 } }],
|
|
168
|
+
}, client as any);
|
|
169
|
+
const call = client.updateBlocksBatch.mock.calls[0] as unknown[];
|
|
170
|
+
const normalized = call[1] as Array<{ attributes: Record<string, unknown> }>;
|
|
171
|
+
expect(normalized[0].attributes).toEqual({ level: 2 });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests: yoast_bulk_update_seo
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Schema: posts required
|
|
6
|
+
* - Validation: rejects empty posts array
|
|
7
|
+
* - Validation: rejects items missing post_id
|
|
8
|
+
* - Request: normalized item list forwarded (extra fields stripped per-item)
|
|
9
|
+
* - Response shape: array of per-post results
|
|
10
|
+
* - Unknown tool throws
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
14
|
+
import { YOAST_TOOLS, handleYoastTool } from '../../../tools/yoast.js';
|
|
15
|
+
import { makeMockClient } from '../../helpers/mock-client.js';
|
|
16
|
+
import { yoastSEOResponse } from '../../fixtures/rest-responses.js';
|
|
17
|
+
|
|
18
|
+
describe('yoast_bulk_update_seo — schema', () => {
|
|
19
|
+
it('exposes yoast_bulk_update_seo tool', () => {
|
|
20
|
+
expect(YOAST_TOOLS.map((t) => t.name)).toContain('yoast_bulk_update_seo');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('requires posts in inputSchema', () => {
|
|
24
|
+
const tool = YOAST_TOOLS.find((t) => t.name === 'yoast_bulk_update_seo')!;
|
|
25
|
+
expect(tool.inputSchema.required).toContain('posts');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe('yoast_bulk_update_seo — validation', () => {
|
|
30
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
31
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
32
|
+
|
|
33
|
+
it('rejects empty posts array', async () => {
|
|
34
|
+
await expect(handleYoastTool('yoast_bulk_update_seo', { posts: [] }, client as any))
|
|
35
|
+
.rejects.toThrow('non-empty');
|
|
36
|
+
expect(client.bulkUpdateYoastSEO).not.toHaveBeenCalled();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('rejects items missing post_id', async () => {
|
|
40
|
+
await expect(handleYoastTool('yoast_bulk_update_seo', {
|
|
41
|
+
posts: [{ title: 'X' }],
|
|
42
|
+
}, client as any)).rejects.toThrow('post_id');
|
|
43
|
+
expect(client.bulkUpdateYoastSEO).not.toHaveBeenCalled();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('yoast_bulk_update_seo — request shape', () => {
|
|
48
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
client = makeMockClient();
|
|
51
|
+
client.bulkUpdateYoastSEO.mockResolvedValue([
|
|
52
|
+
{ post_id: 1, success: true, seo: yoastSEOResponse },
|
|
53
|
+
{ post_id: 2, success: true, seo: { ...yoastSEOResponse, post_id: 2 } },
|
|
54
|
+
]);
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('forwards normalized item list (extra fields stripped per item)', async () => {
|
|
59
|
+
await handleYoastTool('yoast_bulk_update_seo', {
|
|
60
|
+
posts: [
|
|
61
|
+
{ post_id: 1, title: 'A', evil_extra: 'strip me' },
|
|
62
|
+
{ post_id: 2, noindex: false, focus_keyword: 'keyword' },
|
|
63
|
+
],
|
|
64
|
+
}, client as any);
|
|
65
|
+
expect(client.bulkUpdateYoastSEO).toHaveBeenCalledWith([
|
|
66
|
+
{ post_id: 1, title: 'A' },
|
|
67
|
+
{ post_id: 2, noindex: false, focus_keyword: 'keyword' },
|
|
68
|
+
]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('processes multiple posts in a single call', async () => {
|
|
72
|
+
await handleYoastTool('yoast_bulk_update_seo', {
|
|
73
|
+
posts: [
|
|
74
|
+
{ post_id: 10, title: 'Page A' },
|
|
75
|
+
{ post_id: 11, title: 'Page B' },
|
|
76
|
+
{ post_id: 12, description: 'Meta C' },
|
|
77
|
+
],
|
|
78
|
+
}, client as any);
|
|
79
|
+
const [items] = client.bulkUpdateYoastSEO.mock.calls[0] as [unknown[]];
|
|
80
|
+
expect(items).toHaveLength(3);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
describe('yoast_bulk_update_seo — response shape', () => {
|
|
85
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
86
|
+
beforeEach(() => {
|
|
87
|
+
client = makeMockClient();
|
|
88
|
+
client.bulkUpdateYoastSEO.mockResolvedValue([
|
|
89
|
+
{ post_id: 1, success: true, seo: yoastSEOResponse },
|
|
90
|
+
]);
|
|
91
|
+
vi.clearAllMocks();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('returns array of per-post results', async () => {
|
|
95
|
+
const result = await handleYoastTool('yoast_bulk_update_seo', {
|
|
96
|
+
posts: [{ post_id: 1, title: 'X' }],
|
|
97
|
+
}, client as any) as any[];
|
|
98
|
+
expect(Array.isArray(result)).toBe(true);
|
|
99
|
+
expect(result[0].post_id).toBe(1);
|
|
100
|
+
expect(result[0].success).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('yoast_bulk_update_seo — unknown tool', () => {
|
|
105
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
106
|
+
beforeEach(() => { client = makeMockClient(); });
|
|
107
|
+
|
|
108
|
+
it('throws on unknown tool name', async () => {
|
|
109
|
+
await expect(handleYoastTool('unknown_tool', {}, client as any))
|
|
110
|
+
.rejects.toThrow('Unknown yoast tool');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests: yoast_get_seo
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Schema: tool exposed with post_id required
|
|
6
|
+
* - Validation: post_id must be a number
|
|
7
|
+
* - Request: post_id forwarded to client.getYoastSEO
|
|
8
|
+
* - Response shape: post_id, title, description, seo_score, readability_score
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
|
+
import { YOAST_TOOLS, handleYoastTool } from '../../../tools/yoast.js';
|
|
13
|
+
import { makeMockClient } from '../../helpers/mock-client.js';
|
|
14
|
+
import { yoastSEOResponse } from '../../fixtures/rest-responses.js';
|
|
15
|
+
|
|
16
|
+
describe('yoast_get_seo — schema', () => {
|
|
17
|
+
it('exposes yoast_get_seo tool', () => {
|
|
18
|
+
expect(YOAST_TOOLS.map((t) => t.name)).toContain('yoast_get_seo');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('requires post_id in inputSchema', () => {
|
|
22
|
+
const tool = YOAST_TOOLS.find((t) => t.name === 'yoast_get_seo')!;
|
|
23
|
+
expect(tool.inputSchema.required).toContain('post_id');
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('yoast_get_seo — validation', () => {
|
|
28
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
29
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
30
|
+
|
|
31
|
+
it('rejects missing post_id', async () => {
|
|
32
|
+
await expect(handleYoastTool('yoast_get_seo', {}, client as any)).rejects.toThrow('post_id');
|
|
33
|
+
expect(client.getYoastSEO).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects string post_id (must be number)', async () => {
|
|
37
|
+
await expect(handleYoastTool('yoast_get_seo', { post_id: '42' }, client as any))
|
|
38
|
+
.rejects.toThrow('post_id');
|
|
39
|
+
expect(client.getYoastSEO).not.toHaveBeenCalled();
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('yoast_get_seo — request shape', () => {
|
|
44
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
client = makeMockClient();
|
|
47
|
+
client.getYoastSEO.mockResolvedValue(yoastSEOResponse);
|
|
48
|
+
vi.clearAllMocks();
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('forwards post_id to client.getYoastSEO', async () => {
|
|
52
|
+
await handleYoastTool('yoast_get_seo', { post_id: 42 }, client as any);
|
|
53
|
+
expect(client.getYoastSEO).toHaveBeenCalledWith(42);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('yoast_get_seo — response shape', () => {
|
|
58
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
59
|
+
beforeEach(() => {
|
|
60
|
+
client = makeMockClient();
|
|
61
|
+
client.getYoastSEO.mockResolvedValue(yoastSEOResponse);
|
|
62
|
+
vi.clearAllMocks();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('returns post_id, title, description, scores', async () => {
|
|
66
|
+
const result = await handleYoastTool('yoast_get_seo', { post_id: 42 }, client as any) as any;
|
|
67
|
+
expect(result.post_id).toBe(9999);
|
|
68
|
+
expect(result.title).toBe('SEO Title');
|
|
69
|
+
expect(result.description).toBe('Meta description.');
|
|
70
|
+
expect(result.seo_score).toBe(78);
|
|
71
|
+
expect(result.readability_score).toBe(65);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('noindex can be null (tri-state)', async () => {
|
|
75
|
+
const result = await handleYoastTool('yoast_get_seo', { post_id: 42 }, client as any) as any;
|
|
76
|
+
expect(result.noindex).toBeNull();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests: yoast_update_seo
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Schema: post_id required
|
|
6
|
+
* - Validation: post_id required
|
|
7
|
+
* - Validation: at least one mutating Yoast field required
|
|
8
|
+
* - Request: only known Yoast fields forwarded (extra fields stripped)
|
|
9
|
+
* - noindex tri-state: null is preserved
|
|
10
|
+
* - robots_advanced: unknown directives filtered out
|
|
11
|
+
* - schema_page_type: out-of-enum values dropped
|
|
12
|
+
* - Response shape
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
16
|
+
import { YOAST_TOOLS, handleYoastTool } from '../../../tools/yoast.js';
|
|
17
|
+
import { makeMockClient } from '../../helpers/mock-client.js';
|
|
18
|
+
import { yoastSEOResponse } from '../../fixtures/rest-responses.js';
|
|
19
|
+
|
|
20
|
+
describe('yoast_update_seo — schema', () => {
|
|
21
|
+
it('exposes yoast_update_seo tool', () => {
|
|
22
|
+
expect(YOAST_TOOLS.map((t) => t.name)).toContain('yoast_update_seo');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('requires post_id in inputSchema', () => {
|
|
26
|
+
const tool = YOAST_TOOLS.find((t) => t.name === 'yoast_update_seo')!;
|
|
27
|
+
expect(tool.inputSchema.required).toContain('post_id');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('yoast_update_seo — validation', () => {
|
|
32
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
33
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
34
|
+
|
|
35
|
+
it('requires post_id', async () => {
|
|
36
|
+
await expect(handleYoastTool('yoast_update_seo', { title: 'New' }, client as any))
|
|
37
|
+
.rejects.toThrow('post_id');
|
|
38
|
+
expect(client.updateYoastSEO).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('requires at least one Yoast mutating field', async () => {
|
|
42
|
+
await expect(handleYoastTool('yoast_update_seo', { post_id: 1 }, client as any))
|
|
43
|
+
.rejects.toThrow('at least one Yoast field');
|
|
44
|
+
expect(client.updateYoastSEO).not.toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('yoast_update_seo — request shape', () => {
|
|
49
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
client = makeMockClient();
|
|
52
|
+
client.updateYoastSEO.mockResolvedValue({ ...yoastSEOResponse, title: 'New' });
|
|
53
|
+
vi.clearAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('forwards known Yoast fields and strips unknown fields', async () => {
|
|
57
|
+
await handleYoastTool('yoast_update_seo', {
|
|
58
|
+
post_id: 1, title: 'New', noindex: true, evil_extra: 'ignored',
|
|
59
|
+
}, client as any);
|
|
60
|
+
expect(client.updateYoastSEO).toHaveBeenCalledWith(1, { title: 'New', noindex: true });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('preserves null in noindex (tri-state)', async () => {
|
|
64
|
+
await handleYoastTool('yoast_update_seo', { post_id: 1, noindex: null }, client as any);
|
|
65
|
+
expect(client.updateYoastSEO).toHaveBeenCalledWith(1, { noindex: null });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('filters robots_advanced to known directives only', async () => {
|
|
69
|
+
await handleYoastTool('yoast_update_seo', {
|
|
70
|
+
post_id: 1, robots_advanced: ['noarchive', 'evil', 'nosnippet'],
|
|
71
|
+
}, client as any);
|
|
72
|
+
expect(client.updateYoastSEO).toHaveBeenCalledWith(1, {
|
|
73
|
+
robots_advanced: ['noarchive', 'nosnippet'],
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('drops out-of-enum schema_page_type values', async () => {
|
|
78
|
+
await handleYoastTool('yoast_update_seo', {
|
|
79
|
+
post_id: 1, schema_page_type: 'BogusType', title: 'Valid',
|
|
80
|
+
}, client as any);
|
|
81
|
+
expect(client.updateYoastSEO).toHaveBeenCalledWith(1, { title: 'Valid' });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('post_id is the first arg not part of the body', async () => {
|
|
85
|
+
await handleYoastTool('yoast_update_seo', { post_id: 99, title: 'X' }, client as any);
|
|
86
|
+
const [id, body] = client.updateYoastSEO.mock.calls[0] as [number, Record<string, unknown>];
|
|
87
|
+
expect(id).toBe(99);
|
|
88
|
+
expect(body).not.toHaveProperty('post_id');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('yoast_update_seo — response shape', () => {
|
|
93
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
client = makeMockClient();
|
|
96
|
+
client.updateYoastSEO.mockResolvedValue({ ...yoastSEOResponse, title: 'Updated' });
|
|
97
|
+
vi.clearAllMocks();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('returns updated SEO metadata', async () => {
|
|
101
|
+
const result = await handleYoastTool('yoast_update_seo', { post_id: 1, title: 'Updated' }, client as any) as any;
|
|
102
|
+
expect(result.post_id).toBe(9999);
|
|
103
|
+
expect(result.title).toBe('Updated');
|
|
104
|
+
});
|
|
105
|
+
});
|