@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,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests: insert_blocks
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Input validation
|
|
6
|
+
* - Positioning params (after/before/after_ref/before_ref)
|
|
7
|
+
* - Enricher wiring
|
|
8
|
+
* - Warning enrichment (formatted_warnings)
|
|
9
|
+
* - Clean path (no warnings)
|
|
10
|
+
* - Ref in inserted blocks forwarded
|
|
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
|
+
import { assertHasFormattedWarning, assertNoFormattedWarnings } from '../../helpers/request-matchers.js';
|
|
17
|
+
|
|
18
|
+
vi.mock('../../../enrichers.js', () => ({
|
|
19
|
+
enrichBlock: vi.fn(async (block: any) => block),
|
|
20
|
+
enrichBlocks: vi.fn(async (blocks: any[]) =>
|
|
21
|
+
blocks.map((b: any) => ({ ...b, attributes: { ...b.attributes, enriched: true } }))
|
|
22
|
+
),
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
describe('insert_blocks — validation', () => {
|
|
26
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
27
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
28
|
+
|
|
29
|
+
it('requires post_id', async () => {
|
|
30
|
+
await expect(
|
|
31
|
+
handleWriteTool('insert_blocks', { blocks: [{ name: 'core/paragraph' }] }, client as any)
|
|
32
|
+
).rejects.toThrow('post_id');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('requires at least one block', async () => {
|
|
36
|
+
await expect(
|
|
37
|
+
handleWriteTool('insert_blocks', { post_id: 1 }, client as any)
|
|
38
|
+
).rejects.toThrow('block');
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('rejects empty blocks array', async () => {
|
|
42
|
+
await expect(
|
|
43
|
+
handleWriteTool('insert_blocks', { post_id: 1, blocks: [] }, client as any)
|
|
44
|
+
).rejects.toThrow('block');
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('insert_blocks — positioning', () => {
|
|
49
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
50
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
51
|
+
|
|
52
|
+
it('forwards after_top_level as "after" to client', async () => {
|
|
53
|
+
await handleWriteTool('insert_blocks', {
|
|
54
|
+
post_id: 1, after_top_level: 5, blocks: [{ name: 'core/paragraph' }],
|
|
55
|
+
}, client as any);
|
|
56
|
+
expect(client.insertBlocks).toHaveBeenCalledWith(1, expect.objectContaining({ after: 5 }));
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('forwards before_top_level as "before" to client', async () => {
|
|
60
|
+
await handleWriteTool('insert_blocks', {
|
|
61
|
+
post_id: 1, before_top_level: 2, blocks: [{ name: 'core/paragraph' }],
|
|
62
|
+
}, client as any);
|
|
63
|
+
expect(client.insertBlocks).toHaveBeenCalledWith(1, expect.objectContaining({ before: 2 }));
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('forwards after_ref to client body', async () => {
|
|
67
|
+
await handleWriteTool('insert_blocks', {
|
|
68
|
+
post_id: 1, after_ref: 'blk_anchor', blocks: [{ name: 'core/paragraph', innerHTML: '<p>x</p>' }],
|
|
69
|
+
}, client as any);
|
|
70
|
+
const callArg = client.insertBlocks.mock.calls[0][1] as any;
|
|
71
|
+
expect(callArg.after_ref).toBe('blk_anchor');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('forwards before_ref to client body', async () => {
|
|
75
|
+
await handleWriteTool('insert_blocks', {
|
|
76
|
+
post_id: 1, before_ref: 'blk_anchor2', blocks: [{ name: 'core/paragraph', innerHTML: '<p>x</p>' }],
|
|
77
|
+
}, client as any);
|
|
78
|
+
const callArg = client.insertBlocks.mock.calls[0][1] as any;
|
|
79
|
+
expect(callArg.before_ref).toBe('blk_anchor2');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('does not include after_ref/before_ref keys when not provided', async () => {
|
|
83
|
+
await handleWriteTool('insert_blocks', {
|
|
84
|
+
post_id: 1, after_top_level: 2, blocks: [{ name: 'core/paragraph', innerHTML: '<p>x</p>' }],
|
|
85
|
+
}, client as any);
|
|
86
|
+
const callArg = client.insertBlocks.mock.calls[0][1] as any;
|
|
87
|
+
expect(callArg).not.toHaveProperty('after_ref');
|
|
88
|
+
expect(callArg).not.toHaveProperty('before_ref');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('insert_blocks — enricher wiring', () => {
|
|
93
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
94
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
95
|
+
|
|
96
|
+
it('passes blocks through enrichBlocks', async () => {
|
|
97
|
+
const { enrichBlocks } = await import('../../../enrichers.js');
|
|
98
|
+
(enrichBlocks as ReturnType<typeof vi.fn>).mockClear();
|
|
99
|
+
await handleWriteTool('insert_blocks', {
|
|
100
|
+
post_id: 1, blocks: [{ name: 'core/paragraph', attributes: { content: 'Hi' } }],
|
|
101
|
+
}, client as any);
|
|
102
|
+
expect(enrichBlocks).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('enriched attributes reach the client', async () => {
|
|
106
|
+
await handleWriteTool('insert_blocks', {
|
|
107
|
+
post_id: 1, blocks: [{ name: 'core/paragraph', attributes: { content: 'Hi' } }],
|
|
108
|
+
}, client as any);
|
|
109
|
+
const callArg = client.insertBlocks.mock.calls[0][1] as any;
|
|
110
|
+
expect(callArg.blocks[0].attributes).toMatchObject({ content: 'Hi', enriched: true });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe('insert_blocks — warning enrichment', () => {
|
|
115
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
116
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
117
|
+
|
|
118
|
+
it('adds formatted_warnings when response has warnings', async () => {
|
|
119
|
+
client.insertBlocks.mockResolvedValueOnce({
|
|
120
|
+
success: true, inserted: [], before_revision_id: 1, revision_id: 2,
|
|
121
|
+
warnings: [{ block: 'oldns/heading', message: 'AVOID', suggested_replacement: 'core/heading' }],
|
|
122
|
+
});
|
|
123
|
+
const result = await handleWriteTool('insert_blocks', {
|
|
124
|
+
post_id: 1, blocks: [{ name: 'oldns/heading' }],
|
|
125
|
+
}, client as any);
|
|
126
|
+
assertHasFormattedWarning(result, 'WARNING');
|
|
127
|
+
assertHasFormattedWarning(result, 'oldns/heading');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('no formatted_warnings when response has none', async () => {
|
|
131
|
+
const result = await handleWriteTool('insert_blocks', {
|
|
132
|
+
post_id: 1, blocks: [{ name: 'core/heading' }],
|
|
133
|
+
}, client as any);
|
|
134
|
+
assertNoFormattedWarnings(result);
|
|
135
|
+
expect((result as any).success).toBe(true);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns ref on inserted blocks', async () => {
|
|
139
|
+
client.insertBlocks.mockResolvedValueOnce({
|
|
140
|
+
success: true,
|
|
141
|
+
inserted: [{ index: 0, name: 'core/paragraph', ref: 'blk_new001' }],
|
|
142
|
+
warnings: [], before_revision_id: 1, revision_id: 2,
|
|
143
|
+
});
|
|
144
|
+
const result = await handleWriteTool('insert_blocks', {
|
|
145
|
+
post_id: 1, blocks: [{ name: 'core/paragraph' }],
|
|
146
|
+
}, client as any) as any;
|
|
147
|
+
expect(result.inserted[0].ref).toBe('blk_new001');
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests: ref-based addressing for write tools
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - update_block: flat_index path vs ref path (XOR validation + routing)
|
|
6
|
+
* - delete_block: top_level_counter vs ref (XOR validation + routing)
|
|
7
|
+
* - insert_blocks: after_ref / before_ref forwarded to client
|
|
8
|
+
* - NaN / negative integer guards for flat_index and top_level_counter
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
|
+
import { handleWriteTool } from '../../../tools/write.js';
|
|
13
|
+
import { makeMockClient } from '../../helpers/mock-client.js';
|
|
14
|
+
|
|
15
|
+
vi.mock('../../../enrichers.js', () => ({
|
|
16
|
+
enrichBlock: vi.fn(async (block: any) => block),
|
|
17
|
+
enrichBlocks: vi.fn(async (blocks: any[]) => blocks),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
describe('update_block — ref vs flat_index routing', () => {
|
|
21
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
22
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
23
|
+
|
|
24
|
+
it('routes to updateBlock when only flat_index is provided', async () => {
|
|
25
|
+
await handleWriteTool('update_block', { post_id: 1, flat_index: 5, attributes: { level: 3 } }, client as any);
|
|
26
|
+
expect(client.updateBlock).toHaveBeenCalledWith(1, 5, { attributes: { level: 3 }, innerHTML: undefined });
|
|
27
|
+
expect(client.updateBlockByRef).not.toHaveBeenCalled();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('routes to updateBlockByRef when only ref is provided', async () => {
|
|
31
|
+
await handleWriteTool('update_block', { post_id: 1, ref: 'blk_abc12345', attributes: { level: 3 } }, client as any);
|
|
32
|
+
expect(client.updateBlockByRef).toHaveBeenCalledWith(1, 'blk_abc12345', { attributes: { level: 3 }, innerHTML: undefined });
|
|
33
|
+
expect(client.updateBlock).not.toHaveBeenCalled();
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('rejects when both flat_index and ref are provided', async () => {
|
|
37
|
+
await expect(
|
|
38
|
+
handleWriteTool('update_block', { post_id: 1, flat_index: 0, ref: 'blk_abc', attributes: {} }, client as any)
|
|
39
|
+
).rejects.toThrow(/flat_index OR ref, not both/);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('rejects when neither flat_index nor ref is provided', async () => {
|
|
43
|
+
await expect(
|
|
44
|
+
handleWriteTool('update_block', { post_id: 1, attributes: {} }, client as any)
|
|
45
|
+
).rejects.toThrow(/Provide either flat_index/);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('rejects empty-string ref', async () => {
|
|
49
|
+
await expect(
|
|
50
|
+
handleWriteTool('update_block', { post_id: 1, ref: '', attributes: {} }, client as any)
|
|
51
|
+
).rejects.toThrow(/Provide either flat_index/);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('passes innerHTML through ref path', async () => {
|
|
55
|
+
await handleWriteTool('update_block', { post_id: 1, ref: 'blk_x', innerHTML: '<h2>hi</h2>' }, client as any);
|
|
56
|
+
expect(client.updateBlockByRef).toHaveBeenCalledWith(1, 'blk_x', { attributes: undefined, innerHTML: '<h2>hi</h2>' });
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('delete_block — ref vs top_level_counter routing', () => {
|
|
61
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
62
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
63
|
+
|
|
64
|
+
it('routes to deleteBlock when only top_level_counter is provided', async () => {
|
|
65
|
+
await handleWriteTool('delete_block', { post_id: 1, top_level_counter: 3 }, client as any);
|
|
66
|
+
expect(client.deleteBlock).toHaveBeenCalledWith(1, 3, undefined);
|
|
67
|
+
expect(client.deleteBlockByRef).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('routes to deleteBlockByRef when only ref is provided', async () => {
|
|
71
|
+
await handleWriteTool('delete_block', { post_id: 1, ref: 'blk_target' }, client as any);
|
|
72
|
+
expect(client.deleteBlockByRef).toHaveBeenCalledWith(1, 'blk_target', undefined);
|
|
73
|
+
expect(client.deleteBlock).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rejects when both top_level_counter and ref are provided', async () => {
|
|
77
|
+
await expect(
|
|
78
|
+
handleWriteTool('delete_block', { post_id: 1, top_level_counter: 0, ref: 'blk_x' }, client as any)
|
|
79
|
+
).rejects.toThrow(/top_level_counter OR ref, not both/);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('rejects when neither targeting field is provided', async () => {
|
|
83
|
+
await expect(
|
|
84
|
+
handleWriteTool('delete_block', { post_id: 1 }, client as any)
|
|
85
|
+
).rejects.toThrow(/Provide either top_level_counter/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('top_level_counter:0 is a valid target', async () => {
|
|
89
|
+
await handleWriteTool('delete_block', { post_id: 1, top_level_counter: 0 }, client as any);
|
|
90
|
+
expect(client.deleteBlock).toHaveBeenCalledWith(1, 0, undefined);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('forwards count parameter through ref path', async () => {
|
|
94
|
+
await handleWriteTool('delete_block', { post_id: 1, ref: 'blk_x', count: 3 }, client as any);
|
|
95
|
+
expect(client.deleteBlockByRef).toHaveBeenCalledWith(1, 'blk_x', 3);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('insert_blocks — after_ref / before_ref', () => {
|
|
100
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
101
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
102
|
+
|
|
103
|
+
it('forwards after_ref to client', async () => {
|
|
104
|
+
await handleWriteTool('insert_blocks', {
|
|
105
|
+
post_id: 1, after_ref: 'blk_anchor',
|
|
106
|
+
blocks: [{ name: 'core/paragraph', innerHTML: '<p>x</p>' }],
|
|
107
|
+
}, client as any);
|
|
108
|
+
const call = client.insertBlocks.mock.calls[0]![1] as Record<string, unknown>;
|
|
109
|
+
expect(call.after_ref).toBe('blk_anchor');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('forwards before_ref to client', async () => {
|
|
113
|
+
await handleWriteTool('insert_blocks', {
|
|
114
|
+
post_id: 1, before_ref: 'blk_anchor2',
|
|
115
|
+
blocks: [{ name: 'core/paragraph', innerHTML: '<p>x</p>' }],
|
|
116
|
+
}, client as any);
|
|
117
|
+
const call = client.insertBlocks.mock.calls[0]![1] as Record<string, unknown>;
|
|
118
|
+
expect(call.before_ref).toBe('blk_anchor2');
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('does not include after_ref/before_ref when using numeric position', async () => {
|
|
122
|
+
await handleWriteTool('insert_blocks', {
|
|
123
|
+
post_id: 1, after_top_level: 2,
|
|
124
|
+
blocks: [{ name: 'core/paragraph', innerHTML: '<p>x</p>' }],
|
|
125
|
+
}, client as any);
|
|
126
|
+
const call = client.insertBlocks.mock.calls[0]![1] as Record<string, unknown>;
|
|
127
|
+
expect(call).not.toHaveProperty('after_ref');
|
|
128
|
+
expect(call).not.toHaveProperty('before_ref');
|
|
129
|
+
expect(call.after).toBe(2);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('response preserves ref on inserted blocks', async () => {
|
|
133
|
+
client.insertBlocks.mockResolvedValueOnce({
|
|
134
|
+
success: true,
|
|
135
|
+
inserted: [{ index: 0, name: 'core/heading', ref: 'blk_new001' }],
|
|
136
|
+
warnings: [], before_revision_id: 1, revision_id: 2,
|
|
137
|
+
});
|
|
138
|
+
const result = await handleWriteTool('insert_blocks', {
|
|
139
|
+
post_id: 1, blocks: [{ name: 'core/paragraph' }],
|
|
140
|
+
}, client as any) as any;
|
|
141
|
+
expect(result.inserted[0].ref).toBe('blk_new001');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
describe('NaN / integer guards', () => {
|
|
146
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
147
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
148
|
+
|
|
149
|
+
it('update_block rejects NaN flat_index', async () => {
|
|
150
|
+
await expect(
|
|
151
|
+
handleWriteTool('update_block', { post_id: 1, flat_index: NaN, attributes: {} }, client as any)
|
|
152
|
+
).rejects.toThrow(/Provide either flat_index/);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('update_block rejects negative flat_index', async () => {
|
|
156
|
+
await expect(
|
|
157
|
+
handleWriteTool('update_block', { post_id: 1, flat_index: -1, attributes: {} }, client as any)
|
|
158
|
+
).rejects.toThrow(/Provide either flat_index/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('delete_block rejects NaN top_level_counter', async () => {
|
|
162
|
+
await expect(
|
|
163
|
+
handleWriteTool('delete_block', { post_id: 1, top_level_counter: NaN }, client as any)
|
|
164
|
+
).rejects.toThrow(/Provide either top_level_counter/);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('delete_block rejects count=0 (must be positive)', async () => {
|
|
168
|
+
await expect(
|
|
169
|
+
handleWriteTool('delete_block', { post_id: 1, top_level_counter: 0, count: 0 }, client as any)
|
|
170
|
+
).rejects.toThrow(/count must be a positive integer/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('delete_block accepts count=1', async () => {
|
|
174
|
+
await handleWriteTool('delete_block', { post_id: 1, top_level_counter: 0, count: 1 }, client as any);
|
|
175
|
+
expect(client.deleteBlock).toHaveBeenCalledWith(1, 0, 1);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests: replace_block_range
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Input validation (post_id, start, count, blocks)
|
|
6
|
+
* - Client forwarding
|
|
7
|
+
* - Enricher wiring
|
|
8
|
+
* - Warning enrichment
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
12
|
+
import { handleWriteTool } from '../../../tools/write.js';
|
|
13
|
+
import { makeMockClient } from '../../helpers/mock-client.js';
|
|
14
|
+
|
|
15
|
+
vi.mock('../../../enrichers.js', () => ({
|
|
16
|
+
enrichBlock: vi.fn(async (block: any) => block),
|
|
17
|
+
enrichBlocks: vi.fn(async (blocks: any[]) =>
|
|
18
|
+
blocks.map((b: any) => ({ ...b, attributes: { ...b.attributes, enriched: true } }))
|
|
19
|
+
),
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
describe('replace_block_range — validation', () => {
|
|
23
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
24
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
25
|
+
|
|
26
|
+
it('requires post_id', async () => {
|
|
27
|
+
await expect(
|
|
28
|
+
handleWriteTool('replace_block_range', { start: 0, count: 1, blocks: [] }, client as any)
|
|
29
|
+
).rejects.toThrow('post_id');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('requires start', async () => {
|
|
33
|
+
await expect(
|
|
34
|
+
handleWriteTool('replace_block_range', { post_id: 1, count: 1, blocks: [] }, client as any)
|
|
35
|
+
).rejects.toThrow('start');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('requires count', async () => {
|
|
39
|
+
await expect(
|
|
40
|
+
handleWriteTool('replace_block_range', { post_id: 1, start: 0, blocks: [] }, client as any)
|
|
41
|
+
).rejects.toThrow('count');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('requires blocks array', async () => {
|
|
45
|
+
await expect(
|
|
46
|
+
handleWriteTool('replace_block_range', { post_id: 1, start: 0, count: 1 }, client as any)
|
|
47
|
+
).rejects.toThrow('block');
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
describe('replace_block_range — forwarding', () => {
|
|
52
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
client = makeMockClient();
|
|
55
|
+
client.replaceBlocksRange = vi.fn().mockResolvedValue({
|
|
56
|
+
success: true, removed: 1,
|
|
57
|
+
inserted: [{ index: 0, name: 'core/paragraph' }],
|
|
58
|
+
warnings: [],
|
|
59
|
+
before_revision_id: 1, revision_id: 2,
|
|
60
|
+
}) as any;
|
|
61
|
+
vi.clearAllMocks();
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('calls replaceBlocksRange with start, count, and blocks', async () => {
|
|
65
|
+
await handleWriteTool('replace_block_range', {
|
|
66
|
+
post_id: 1, start: 2, count: 3,
|
|
67
|
+
blocks: [{ name: 'core/paragraph', attributes: {} }],
|
|
68
|
+
}, client as any);
|
|
69
|
+
expect(client.replaceBlocksRange).toHaveBeenCalledWith(1, expect.objectContaining({
|
|
70
|
+
start: 2, count: 3,
|
|
71
|
+
}));
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('passes blocks through enrichBlocks', async () => {
|
|
75
|
+
const { enrichBlocks } = await import('../../../enrichers.js');
|
|
76
|
+
(enrichBlocks as ReturnType<typeof vi.fn>).mockClear();
|
|
77
|
+
await handleWriteTool('replace_block_range', {
|
|
78
|
+
post_id: 1, start: 0, count: 1,
|
|
79
|
+
blocks: [{ name: 'core/paragraph', attributes: {} }],
|
|
80
|
+
}, client as any);
|
|
81
|
+
expect(enrichBlocks).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('accepts empty blocks array (pure deletion)', async () => {
|
|
85
|
+
await handleWriteTool('replace_block_range', {
|
|
86
|
+
post_id: 1, start: 0, count: 2, blocks: [],
|
|
87
|
+
}, client as any);
|
|
88
|
+
expect(client.replaceBlocksRange).toHaveBeenCalled();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool tests: rewrite_post_blocks and revert_to_revision
|
|
3
|
+
*
|
|
4
|
+
* rewrite_post_blocks: full page rewrite via replaceAllBlocks
|
|
5
|
+
* revert_to_revision: revision rollback
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
9
|
+
import { handleWriteTool } from '../../../tools/write.js';
|
|
10
|
+
import { makeMockClient } from '../../helpers/mock-client.js';
|
|
11
|
+
import { assertHasFormattedWarning, assertNoFormattedWarnings } from '../../helpers/request-matchers.js';
|
|
12
|
+
|
|
13
|
+
vi.mock('../../../enrichers.js', () => ({
|
|
14
|
+
enrichBlock: vi.fn(async (block: any) => block),
|
|
15
|
+
enrichBlocks: vi.fn(async (blocks: any[]) =>
|
|
16
|
+
blocks.map((b: any) => ({ ...b, attributes: { ...b.attributes, enriched: true } }))
|
|
17
|
+
),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
// ── rewrite_post_blocks ───────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
describe('rewrite_post_blocks — validation', () => {
|
|
23
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
24
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
25
|
+
|
|
26
|
+
it('requires post_id', async () => {
|
|
27
|
+
await expect(
|
|
28
|
+
handleWriteTool('rewrite_post_blocks', { blocks: [{ name: 'core/paragraph' }] }, client as any)
|
|
29
|
+
).rejects.toThrow('post_id');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('requires at least one block', async () => {
|
|
33
|
+
await expect(
|
|
34
|
+
handleWriteTool('rewrite_post_blocks', { post_id: 1 }, client as any)
|
|
35
|
+
).rejects.toThrow('block');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('rejects empty blocks array', async () => {
|
|
39
|
+
await expect(
|
|
40
|
+
handleWriteTool('rewrite_post_blocks', { post_id: 1, blocks: [] }, client as any)
|
|
41
|
+
).rejects.toThrow('block');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('rewrite_post_blocks — forwarding', () => {
|
|
46
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
47
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
48
|
+
|
|
49
|
+
it('calls replaceAllBlocks with the block array', async () => {
|
|
50
|
+
const blocks = [{ name: 'core/heading', attributes: { level: 1 } }];
|
|
51
|
+
await handleWriteTool('rewrite_post_blocks', { post_id: 1, blocks }, client as any);
|
|
52
|
+
expect(client.replaceAllBlocks).toHaveBeenCalledWith(1, expect.arrayContaining([
|
|
53
|
+
expect.objectContaining({ name: 'core/heading' }),
|
|
54
|
+
]));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('passes blocks through enrichBlocks', async () => {
|
|
58
|
+
const { enrichBlocks } = await import('../../../enrichers.js');
|
|
59
|
+
(enrichBlocks as ReturnType<typeof vi.fn>).mockClear();
|
|
60
|
+
await handleWriteTool('rewrite_post_blocks', {
|
|
61
|
+
post_id: 1, blocks: [{ name: 'core/heading', attributes: { level: 1 } }],
|
|
62
|
+
}, client as any);
|
|
63
|
+
expect(enrichBlocks).toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('enriched attributes reach replaceAllBlocks', async () => {
|
|
67
|
+
await handleWriteTool('rewrite_post_blocks', {
|
|
68
|
+
post_id: 1, blocks: [{ name: 'core/heading', attributes: { level: 1 } }],
|
|
69
|
+
}, client as any);
|
|
70
|
+
const call = client.replaceAllBlocks.mock.calls[0] as unknown[];
|
|
71
|
+
const blocks = call[1] as Array<{ attributes: Record<string, unknown> }>;
|
|
72
|
+
expect(blocks[0].attributes).toMatchObject({ level: 1, enriched: true });
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('rewrite_post_blocks — warnings', () => {
|
|
77
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
78
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
79
|
+
|
|
80
|
+
it('adds formatted_warnings when response has warnings', async () => {
|
|
81
|
+
client.replaceAllBlocks.mockResolvedValueOnce({
|
|
82
|
+
success: true, inserted: [], before_revision_id: 1, revision_id: 2,
|
|
83
|
+
warnings: [{ block: 'oldns/text', message: 'AVOID', suggested_replacement: 'core/paragraph' }],
|
|
84
|
+
});
|
|
85
|
+
const result = await handleWriteTool('rewrite_post_blocks', {
|
|
86
|
+
post_id: 1, blocks: [{ name: 'oldns/text' }],
|
|
87
|
+
}, client as any);
|
|
88
|
+
assertHasFormattedWarning(result, 'WARNING');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('no formatted_warnings when response is clean', async () => {
|
|
92
|
+
const result = await handleWriteTool('rewrite_post_blocks', {
|
|
93
|
+
post_id: 1, blocks: [{ name: 'core/heading', attributes: { level: 1 } }],
|
|
94
|
+
}, client as any);
|
|
95
|
+
assertNoFormattedWarnings(result);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// ── revert_to_revision ────────────────────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe('revert_to_revision — validation', () => {
|
|
102
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
103
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
104
|
+
|
|
105
|
+
it('requires post_id', async () => {
|
|
106
|
+
await expect(
|
|
107
|
+
handleWriteTool('revert_to_revision', { revision_id: 1 }, client as any)
|
|
108
|
+
).rejects.toThrow('post_id');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('requires revision_id', async () => {
|
|
112
|
+
await expect(
|
|
113
|
+
handleWriteTool('revert_to_revision', { post_id: 1 }, client as any)
|
|
114
|
+
).rejects.toThrow('revision_id');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('revert_to_revision — forwarding', () => {
|
|
119
|
+
let client: ReturnType<typeof makeMockClient>;
|
|
120
|
+
beforeEach(() => { client = makeMockClient(); vi.clearAllMocks(); });
|
|
121
|
+
|
|
122
|
+
it('calls revertToRevision with correct args', async () => {
|
|
123
|
+
await handleWriteTool('revert_to_revision', { post_id: 1, revision_id: 456 }, client as any);
|
|
124
|
+
expect(client.revertToRevision).toHaveBeenCalledWith(1, 456);
|
|
125
|
+
});
|
|
126
|
+
});
|