This repository has no description
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
5import { spawn } from 'node:child_process';
6import { run } from './helpers.js';
7
8let server;
9let port;
10
11beforeAll(async () => {
12 const serverScript = `
13 const server = Bun.serve({
14 port: 0,
15 fetch(req) {
16 const url = new URL(req.url);
17 const path = url.pathname;
18
19 if (path === '/api/stats') {
20 return Response.json({
21 total_caps: 42, total_skills: 10, total_vouches: 5,
22 total_beacons: 3, active_dids: 8, skill_publishers: 4,
23 });
24 }
25
26 if (path === '/api/caps') {
27 return Response.json({
28 caps: [{ title: 'Test Cap', ref: 'test-cap', handle: 'test.bsky.social',
29 beacon: 'vit:github.com/test/repo', description: 'A test cap' }],
30 });
31 }
32
33 if (path === '/api/skills') {
34 return Response.json({
35 skills: [{ name: 'test-skill', version: '1.0.0', description: 'A test skill',
36 handle: 'test.bsky.social' }],
37 });
38 }
39
40 if (path === '/api/beacons') {
41 return Response.json({
42 beacons: [{ beacon: 'vit:github.com/test/repo', handle: 'test.bsky.social' }],
43 });
44 }
45
46 if (path === '/api/cap') {
47 const ref = url.searchParams.get('ref');
48 if (ref === 'network-content-seeding') {
49 return Response.json({
50 cap: {
51 ref: 'network-content-seeding', title: 'Network Content Seeding',
52 beacon: 'vit:github.com/solpbc/vit', handle: 'test.bsky.social',
53 description: 'Seed content across the network',
54 record_json: JSON.stringify({ kind: 'feat', text: 'test body' }),
55 created_at: '2026-01-01T00:00:00Z', vouch_count: 0,
56 },
57 });
58 }
59 return Response.json({});
60 }
61
62 if (path === '/api/skill') {
63 const name = url.searchParams.get('name');
64 if (name === 'atproto-records') {
65 return Response.json({
66 skill: {
67 name: 'atproto-records', version: '1.0.0',
68 description: 'AT Protocol record helpers',
69 handle: 'test.bsky.social', tags: 'atproto',
70 record_json: JSON.stringify({ license: 'MIT' }),
71 vouch_count: 0,
72 },
73 });
74 }
75 return Response.json({});
76 }
77
78 return new Response('Not Found', { status: 404 });
79 },
80 });
81
82 console.log(server.port);
83 `;
84
85 server = spawn('bun', ['-e', serverScript], { stdio: ['ignore', 'pipe', 'inherit'] });
86 port = await new Promise((resolve, reject) => {
87 let started = false;
88 const timer = setTimeout(() => {
89 server.kill();
90 reject(new Error('mock explore server failed to start'));
91 }, 5000);
92
93 server.stdout.setEncoding('utf-8');
94 server.stdout.on('data', (chunk) => {
95 if (started) return;
96 const value = Number(String(chunk).trim().split(/\r?\n/, 1)[0]);
97 if (!Number.isNaN(value) && value > 0) {
98 started = true;
99 clearTimeout(timer);
100 resolve(value);
101 }
102 });
103 server.once('exit', (code) => {
104 if (!started) {
105 clearTimeout(timer);
106 reject(new Error(`mock explore server exited early: ${code}`));
107 }
108 });
109 });
110});
111
112afterAll(async () => {
113 if (!server || server.exitCode !== null) return;
114 await new Promise((resolve) => {
115 server.once('exit', () => resolve());
116 server.kill();
117 });
118});
119
120describe('vit explore', () => {
121 test('shows help', () => {
122 const result = run('explore --help', '/tmp');
123 expect(result.exitCode).toBe(0);
124 expect(result.stdout).toContain('explore');
125 });
126
127 test('stats returns JSON', () => {
128 const result = run(`explore stats --json --explore-url http://localhost:${port}`, '/tmp');
129 expect(result.exitCode).toBe(0);
130 const data = JSON.parse(result.stdout);
131 expect(data.ok).toBe(true);
132 expect(typeof data.total_caps).toBe('number');
133 });
134
135 test('caps returns JSON', () => {
136 const result = run(`explore caps --json --limit 2 --explore-url http://localhost:${port}`, '/tmp');
137 expect(result.exitCode).toBe(0);
138 const data = JSON.parse(result.stdout);
139 expect(data.ok).toBe(true);
140 expect(Array.isArray(data.caps)).toBe(true);
141 });
142
143 test('skills returns JSON', () => {
144 const result = run(`explore skills --json --limit 2 --explore-url http://localhost:${port}`, '/tmp');
145 expect(result.exitCode).toBe(0);
146 const data = JSON.parse(result.stdout);
147 expect(data.ok).toBe(true);
148 expect(Array.isArray(data.skills)).toBe(true);
149 });
150
151 test('beacons returns JSON', () => {
152 const result = run(`explore beacons --json --explore-url http://localhost:${port}`, '/tmp');
153 expect(result.exitCode).toBe(0);
154 const data = JSON.parse(result.stdout);
155 expect(data.ok).toBe(true);
156 expect(Array.isArray(data.beacons)).toBe(true);
157 });
158
159 test('graceful error on unreachable URL', () => {
160 const result = run('explore stats --explore-url http://localhost:1 --json', '/tmp');
161 expect(result.exitCode).not.toBe(0);
162 const data = JSON.parse(result.stdout);
163 expect(data.ok).toBe(false);
164 expect(data.error).toContain('request to http://localhost:1/api/stats failed');
165 });
166
167 test('graceful error on invalid URL', () => {
168 const result = run('explore stats --explore-url not-a-url --json', '/tmp');
169 expect(result.exitCode).not.toBe(0);
170 const data = JSON.parse(result.stdout);
171 expect(data.ok).toBe(false);
172 expect(data.error).toContain('not-a-url');
173 });
174
175 test('vouches requires --cap or --ref', () => {
176 const result = run('explore vouches --json', '/tmp');
177 expect(result.exitCode).not.toBe(0);
178 const data = JSON.parse(result.stdout);
179 expect(data.ok).toBe(false);
180 });
181
182 test('env var override works', () => {
183 const result = run('explore stats --json', '/tmp', { VIT_EXPLORE_URL: 'http://localhost:1' });
184 expect(result.exitCode).not.toBe(0);
185 const data = JSON.parse(result.stdout);
186 expect(data.ok).toBe(false);
187 expect(data.error).toContain('request to http://localhost:1/api/stats failed');
188 });
189
190 test('flag overrides env var', () => {
191 const result = run(
192 `explore stats --json --explore-url http://localhost:${port}`,
193 '/tmp',
194 { VIT_EXPLORE_URL: 'http://localhost:1' },
195 );
196 expect(result.exitCode).toBe(0);
197 const data = JSON.parse(result.stdout);
198 expect(data.ok).toBe(true);
199 });
200
201 test('cap detail returns JSON', () => {
202 const result = run(`explore cap network-content-seeding --json --explore-url http://localhost:${port}`, '/tmp');
203 expect(result.exitCode).toBe(0);
204 const data = JSON.parse(result.stdout);
205 expect(data.ok).toBe(true);
206 expect(data.cap).toBeDefined();
207 expect(data.cap.ref).toBe('network-content-seeding');
208 expect(data.cap.title).toBeDefined();
209 });
210
211 test('cap detail with beacon', () => {
212 const result = run(
213 `explore cap network-content-seeding --beacon vit:github.com/solpbc/vit --json --explore-url http://localhost:${port}`,
214 '/tmp',
215 );
216 expect(result.exitCode).toBe(0);
217 const data = JSON.parse(result.stdout);
218 expect(data.ok).toBe(true);
219 expect(data.cap).toBeDefined();
220 expect(data.cap.ref).toBe('network-content-seeding');
221 });
222
223 test('cap not found', () => {
224 const result = run(`explore cap nonexistent-ref-xyz --json --explore-url http://localhost:${port}`, '/tmp');
225 expect(result.exitCode).not.toBe(0);
226 const data = JSON.parse(result.stdout);
227 expect(data.ok).toBe(false);
228 expect(data.error).toContain("no cap found with ref 'nonexistent-ref-xyz'");
229 });
230
231 test('skill detail returns JSON', () => {
232 const result = run(`explore skill atproto-records --json --explore-url http://localhost:${port}`, '/tmp');
233 expect(result.exitCode).toBe(0);
234 const data = JSON.parse(result.stdout);
235 expect(data.ok).toBe(true);
236 expect(data.skill).toBeDefined();
237 expect(data.skill.name).toBe('atproto-records');
238 expect(data.skill.version).toBeDefined();
239 });
240
241 test('skill not found', () => {
242 const result = run(`explore skill nonexistent-skill-xyz --json --explore-url http://localhost:${port}`, '/tmp');
243 expect(result.exitCode).not.toBe(0);
244 const data = JSON.parse(result.stdout);
245 expect(data.ok).toBe(false);
246 expect(data.error).toContain("no skill found with name 'nonexistent-skill-xyz'");
247 });
248
249 test('caps --kind filter passes kind to API', () => {
250 const result = run(`explore caps --kind request --json --limit 2 --explore-url http://localhost:${port}`, '/tmp');
251 expect(result.exitCode).toBe(0);
252 const data = JSON.parse(result.stdout);
253 expect(data.ok).toBe(true);
254 expect(Array.isArray(data.caps)).toBe(true);
255 });
256
257 test('caps gracefully degrades on unreachable URL with --kind', () => {
258 const result = run('explore caps --kind request --explore-url http://localhost:1 --json', '/tmp');
259 expect(result.exitCode).not.toBe(0);
260 const data = JSON.parse(result.stdout);
261 expect(data.ok).toBe(false);
262 expect(data.error).toContain('request to http://localhost:1/api/caps?kind=request failed');
263 });
264
265 test('bare explore returns stats JSON', () => {
266 const result = run(`explore --json --explore-url http://localhost:${port}`, '/tmp');
267 expect(result.exitCode).toBe(0);
268 const data = JSON.parse(result.stdout);
269 expect(data.ok).toBe(true);
270 expect(typeof data.total_caps).toBe('number');
271 });
272});