upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/tests/integration/test-cvm-mcp-relay.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'tests/integration/test-cvm-mcp-relay.mjs')
-rw-r--r--tests/integration/test-cvm-mcp-relay.mjs200
1 files changed, 200 insertions, 0 deletions
diff --git a/tests/integration/test-cvm-mcp-relay.mjs b/tests/integration/test-cvm-mcp-relay.mjs
new file mode 100644
index 0000000..e5f42ba
--- /dev/null
+++ b/tests/integration/test-cvm-mcp-relay.mjs
@@ -0,0 +1,200 @@
1import { execSync } from 'child_process';
2
3const IP = process.env.TOLLGATE_IP || '10.192.45.1';
4const RELAY = 'wss://relay.primal.net';
5const OWNER_NSEC = 'a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2';
6
7let passed = 0, failed = 0;
8
9function assert(condition, test) {
10 if (condition) { console.log(` \u2713 ${test}`); passed++; }
11 else { console.log(` \u2717 ${test}`); failed++; }
12}
13
14function nak(args, timeout = 15000) {
15 try {
16 return execSync(`timeout ${timeout / 1000} nak ${args}`, {
17 encoding: 'utf8',
18 stdio: ['pipe', 'pipe', 'pipe'],
19 timeout
20 }).trim();
21 } catch (e) {
22 return e.stdout ? e.stdout.trim() : '';
23 }
24}
25
26function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
27
28function generateId() {
29 return Math.floor(Math.random() * 100000);
30}
31
32async function sendMcpRequest(nsec, method, params, timeout = 15000) {
33 const id = generateId();
34 const content = JSON.stringify({ jsonrpc: '2.0', id, method, params: params || {} });
35
36 const boardNpub = nak(`key public ${OWNER_NSEC}`);
37
38 const result = nak(
39 `event -k 25910 -c '${content.replace(/'/g, "'\\''")}' -p ${boardNpub} --sec ${nsec} ${RELAY}`,
40 timeout
41 );
42
43 const reqEventId = result.match(/"id":"([a-f0-9]{64})"/);
44 return { reqEventId: reqEventId ? reqEventId[1] : null, id, content };
45}
46
47async function waitForResponse(reqEventId, timeout = 20000) {
48 const start = Date.now();
49 while (Date.now() - start < timeout) {
50 const result = nak(
51 `req -k 25910 -l 5 --tag e=${reqEventId} ${RELAY}`,
52 8000
53 );
54 if (result.length > 0 && result.includes('"id"')) {
55 return result;
56 }
57 await sleep(2000);
58 }
59 return null;
60}
61
62function extractMcpContent(rawEvent) {
63 try {
64 const lines = rawEvent.split('\n').filter(l => l.trim().startsWith('{'));
65 for (const line of lines) {
66 try {
67 const event = JSON.parse(line);
68 if (event.content) {
69 return JSON.parse(event.content);
70 }
71 } catch {}
72 }
73 } catch {}
74 return null;
75}
76
77async function runTests() {
78 console.log(`\n=== CVM MCP Relay Integration Tests ===`);
79 console.log(`Target: ${IP}`);
80 console.log(`Relay: ${RELAY}\n`);
81
82 const boardNpub = nak(`key public ${OWNER_NSEC}`);
83 console.log(`Board npub: ${boardNpub}`);
84 assert(boardNpub.length === 64, 'Board npub derived correctly');
85
86 console.log('\n--- Pre-flight: Board is reachable ---');
87 try {
88 const apiResult = execSync(`curl -s --connect-timeout 5 http://${IP}:2121/usage`, { encoding: 'utf8', timeout: 10000 });
89 assert(apiResult.length > 0, 'API /usage responds (board online)');
90 } catch (e) {
91 console.log(' WARNING: Board API not reachable. Tests may fail.');
92 }
93
94 console.log('\n--- Test 1: MCP initialize via relay ---');
95 {
96 const { reqEventId } = await sendMcpRequest(OWNER_NSEC, 'initialize');
97 assert(reqEventId !== null, 'Initialize request published');
98 if (reqEventId) {
99 const resp = await waitForResponse(reqEventId);
100 if (resp) {
101 assert(resp.includes('protocolVersion'), 'Response has protocolVersion');
102 assert(resp.includes('TollGate'), 'Response has server name TollGate');
103 } else {
104 assert(false, 'Got response for initialize');
105 }
106 }
107 }
108
109 console.log('\n--- Test 2: get_sessions via relay ---');
110 {
111 const { reqEventId } = await sendMcpRequest(OWNER_NSEC, 'tools/call', { name: 'get_sessions' });
112 assert(reqEventId !== null, 'get_sessions request published');
113 if (reqEventId) {
114 const resp = await waitForResponse(reqEventId);
115 if (resp) {
116 assert(resp.includes('content'), 'Response has content');
117 const mcp = extractMcpContent(resp);
118 if (mcp && mcp.result && mcp.result.content) {
119 const text = mcp.result.content[0]?.text;
120 if (text) {
121 const sessions = JSON.parse(text);
122 assert(Array.isArray(sessions), 'get_sessions returns array');
123 console.log(` Active sessions: ${sessions.filter(s => s.active).length}`);
124 }
125 } else {
126 assert(false, 'get_sessions MCP response parseable');
127 }
128 } else {
129 assert(false, 'Got response for get_sessions');
130 }
131 }
132 }
133
134 console.log('\n--- Test 3: get_usage via relay ---');
135 {
136 const { reqEventId } = await sendMcpRequest(OWNER_NSEC, 'tools/call', { name: 'get_usage' });
137 assert(reqEventId !== null, 'get_usage request published');
138 if (reqEventId) {
139 const resp = await waitForResponse(reqEventId);
140 if (resp) {
141 assert(resp.includes('content'), 'Response has content');
142 const mcp = extractMcpContent(resp);
143 if (mcp && mcp.result && mcp.result.content) {
144 const text = mcp.result.content[0]?.text;
145 if (text) {
146 const usage = JSON.parse(text);
147 assert(usage.metric !== undefined, 'get_usage returns metric');
148 assert(usage.price_per_step !== undefined, 'get_usage returns price_per_step');
149 assert(usage.step_size_ms !== undefined, 'get_usage returns step_size_ms');
150 console.log(` metric=${usage.metric}, price=${usage.price_per_step}, step=${usage.step_size_ms}ms`);
151 }
152 } else {
153 assert(false, 'get_usage MCP response parseable');
154 }
155 } else {
156 assert(false, 'Got response for get_usage');
157 }
158 }
159 }
160
161 console.log('\n--- Test 4: Non-owner auth rejection ---');
162 {
163 const throwawayNsec = nak('key generate');
164 const throwawayNpub = nak(`key public ${throwawayNsec}`);
165 console.log(` Throwaway npub: ${throwawayNpub.substring(0, 16)}...`);
166 assert(throwawayNsec.length === 64, 'Generated throwaway nsec');
167
168 const { reqEventId } = await sendMcpRequest(throwawayNsec, 'tools/call', { name: 'get_config' });
169 assert(reqEventId !== null, 'Non-owner request published');
170 if (reqEventId) {
171 const resp = await waitForResponse(reqEventId, 12000);
172 if (resp && resp.includes('"id"')) {
173 assert(false, 'Non-owner should NOT get a response');
174 } else {
175 assert(true, 'Non-owner correctly ignored (no response)');
176 }
177 }
178
179 await sleep(2000);
180
181 console.log(' Control: sending owner request to verify board still responsive...');
182 const { reqEventId: ctrlId } = await sendMcpRequest(OWNER_NSEC, 'tools/call', { name: 'get_config' });
183 if (ctrlId) {
184 const ctrlResp = await waitForResponse(ctrlId);
185 if (ctrlResp) {
186 assert(true, 'Owner control request after rejection test: PASS');
187 } else {
188 assert(false, 'Owner control request should get response');
189 }
190 }
191 }
192
193 console.log(`\n=== Results: ${passed} passed, ${failed} failed ===\n`);
194 process.exit(failed > 0 ? 1 : 0);
195}
196
197runTests().catch(e => {
198 console.error('Test error:', e.message);
199 process.exit(1);
200});