mcp-hosted-vault-sync.test.mjs
166 lines 5.4 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Hosted MCP vault_sync — POST JSON to bridge /api/v1/vault/sync (parity with Hub proxy).
3 */
4
5 import { describe, it, beforeEach, afterEach } from 'node:test';
6 import assert from 'node:assert/strict';
7 import { Client } from '@modelcontextprotocol/sdk/client/index.js';
8 import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js';
9 import { createHostedMcpServer } from '../hub/gateway/mcp-hosted-server.mjs';
10
11 const BRIDGE_URL = 'http://bridge.test:4321';
12 const CANISTER_URL = 'http://canister.test:4322';
13
14 function makeCtx(overrides = {}) {
15 return {
16 userId: 'u-sync',
17 vaultId: 'v-sync',
18 role: 'editor',
19 token: 'tok-vault-sync',
20 canisterUrl: CANISTER_URL,
21 bridgeUrl: BRIDGE_URL,
22 ...overrides,
23 };
24 }
25
26 function installFetchMock(response = { ok: true, message: 'Synced', notesCount: 0, proposalsCount: 0 }) {
27 const calls = [];
28 const origFetch = globalThis.fetch;
29 globalThis.fetch = async (url, init) => {
30 calls.push({ url: String(url), init });
31 return {
32 ok: true,
33 status: 200,
34 json: async () => response,
35 text: async () => JSON.stringify(response),
36 };
37 };
38 return {
39 calls,
40 restore() {
41 globalThis.fetch = origFetch;
42 },
43 };
44 }
45
46 async function connectEditor(mock) {
47 const mcpServer = createHostedMcpServer(makeCtx());
48 const client = new Client({ name: 'vault-sync-test', version: '0.0.1' });
49 const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
50 await mcpServer.connect(serverTransport);
51 await client.connect(clientTransport);
52 return { client, mcpServer, clientTransport, serverTransport };
53 }
54
55 describe('hosted MCP vault_sync — bridge request shape', () => {
56 let mock;
57 let client;
58
59 beforeEach(() => {
60 mock = installFetchMock();
61 });
62
63 afterEach(async () => {
64 mock.restore();
65 try {
66 await client?.close();
67 } catch (_) {}
68 });
69
70 it('sends POST to {bridgeUrl}/api/v1/vault/sync', async () => {
71 ({ client } = await connectEditor(mock));
72 await client.callTool({ name: 'vault_sync', arguments: {} });
73
74 assert.equal(mock.calls.length, 1);
75 assert.equal(mock.calls[0].url, `${BRIDGE_URL}/api/v1/vault/sync`);
76 assert.equal(mock.calls[0].init.method, 'POST');
77 });
78
79 it('sends Authorization: Bearer and X-Vault-Id', async () => {
80 ({ client } = await connectEditor(mock));
81 await client.callTool({ name: 'vault_sync', arguments: {} });
82
83 const h = mock.calls[0].init.headers;
84 assert.equal(h['Authorization'], 'Bearer tok-vault-sync');
85 assert.equal(h['X-Vault-Id'], 'v-sync');
86 });
87
88 it('sends Content-Type: application/json', async () => {
89 ({ client } = await connectEditor(mock));
90 await client.callTool({ name: 'vault_sync', arguments: {} });
91
92 assert.equal(mock.calls[0].init.headers['Content-Type'], 'application/json');
93 });
94
95 it('sends empty JSON object when repo omitted', async () => {
96 ({ client } = await connectEditor(mock));
97 await client.callTool({ name: 'vault_sync', arguments: {} });
98
99 const body = JSON.parse(mock.calls[0].init.body);
100 assert.deepEqual(body, {});
101 });
102
103 it('sends { repo } when repo provided', async () => {
104 ({ client } = await connectEditor(mock));
105 await client.callTool({ name: 'vault_sync', arguments: { repo: 'acme/notes' } });
106
107 const body = JSON.parse(mock.calls[0].init.body);
108 assert.deepEqual(body, { repo: 'acme/notes' });
109 });
110
111 it('trims repo and omits body.repo when repo is whitespace-only', async () => {
112 ({ client } = await connectEditor(mock));
113 await client.callTool({ name: 'vault_sync', arguments: { repo: ' \t ' } });
114
115 const body = JSON.parse(mock.calls[0].init.body);
116 assert.deepEqual(body, {});
117 });
118
119 it('returns upstream JSON as tool content', async () => {
120 mock.restore();
121 mock = installFetchMock({ ok: true, message: 'Synced', notesCount: 3, proposalsCount: 1 });
122 ({ client } = await connectEditor(mock));
123 const result = await client.callTool({ name: 'vault_sync', arguments: {} });
124
125 assert.ok(result.content?.length);
126 const parsed = JSON.parse(result.content[0].text);
127 assert.equal(parsed.notesCount, 3);
128 assert.equal(parsed.proposalsCount, 1);
129 });
130
131 it('returns isError on upstream failure', async () => {
132 mock.restore();
133 const origFetch = globalThis.fetch;
134 globalThis.fetch = async () => ({
135 ok: false,
136 status: 400,
137 json: async () => ({ error: 'GitHub not connected', code: 'GITHUB_NOT_CONNECTED' }),
138 text: async () => '{"error":"GitHub not connected"}',
139 });
140 mock = { calls: [], restore: () => { globalThis.fetch = origFetch; } };
141
142 ({ client } = await connectEditor(mock));
143 const result = await client.callTool({ name: 'vault_sync', arguments: {} });
144
145 assert.equal(result.isError, true);
146 });
147
148 it('admin role also registers vault_sync', async () => {
149 mock.restore();
150 mock = installFetchMock();
151 const mcpServer = createHostedMcpServer(makeCtx({ role: 'admin' }));
152 const c = new Client({ name: 'vault-sync-admin', version: '0.0.1' });
153 const [ct, st] = InMemoryTransport.createLinkedPair();
154 await mcpServer.connect(st);
155 await c.connect(ct);
156 try {
157 const { tools } = await c.listTools();
158 assert.ok(tools.some((t) => t.name === 'vault_sync'));
159 await c.callTool({ name: 'vault_sync', arguments: { repo: 'o/r' } });
160 assert.equal(mock.calls[0].url, `${BRIDGE_URL}/api/v1/vault/sync`);
161 } finally {
162 await c.close();
163 mock.restore();
164 }
165 });
166 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago