open source is social v-it.org
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
5import { run } from './helpers.js';
6import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
7import { tmpdir } from 'node:os';
8import { join } from 'node:path';
9
10const agentEnv = { CLAUDECODE: '1' };
11
12function parseJson(stdout) {
13 return JSON.parse(stdout);
14}
15
16describe('--json flag', () => {
17 let tmpDir;
18
19 beforeEach(() => {
20 tmpDir = join(tmpdir(), '.test-json-' + Math.random().toString(36).slice(2));
21 mkdirSync(join(tmpDir, '.vit'), { recursive: true });
22 });
23
24 afterEach(() => {
25 rmSync(tmpDir, { recursive: true, force: true });
26 });
27
28 describe('init --json', () => {
29 test('reports status as JSON when not initialized', () => {
30 const r = run('init --json', tmpDir, agentEnv);
31 const j = parseJson(r.stdout);
32 expect(j.ok).toBe(true);
33 expect(j.status).toBe('no beacon');
34 });
35
36 test('reports beacon as JSON when set', () => {
37 run('init --beacon https://github.com/solpbc/vit.git', tmpDir, agentEnv);
38 const r = run('init --json', tmpDir, agentEnv);
39 const j = parseJson(r.stdout);
40 expect(j.ok).toBe(true);
41 expect(j.beacon).toContain('github.com/solpbc/vit');
42 });
43
44 test('creates beacon and returns JSON', () => {
45 const r = run('init --json --beacon https://github.com/solpbc/vit.git', tmpDir, agentEnv);
46 const j = parseJson(r.stdout);
47 expect(j.ok).toBe(true);
48 expect(j.beacon).toContain('github.com/solpbc/vit');
49 });
50
51 test('rejects non-agent with JSON error', () => {
52 const r = run('init --json --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' });
53 expect(r.exitCode).toBe(1);
54 const j = parseJson(r.stdout);
55 expect(j.ok).toBe(false);
56 expect(j.error).toContain('agent required');
57 });
58 });
59
60 describe('doctor --json', () => {
61 test('returns health report as JSON', () => {
62 const r = run('doctor --json');
63 const j = parseJson(r.stdout);
64 expect(j.ok).toBe(true);
65 expect(j).toHaveProperty('install');
66 expect(j).toHaveProperty('beacon');
67 expect(j).toHaveProperty('bluesky');
68 });
69
70 test('status --json also works', () => {
71 const r = run('status --json');
72 const j = parseJson(r.stdout);
73 expect(j.ok).toBe(true);
74 });
75 });
76
77 describe('following --json', () => {
78 test('returns empty list as JSON', () => {
79 const r = run('following --json', tmpDir);
80 const j = parseJson(r.stdout);
81 expect(j.ok).toBe(true);
82 expect(j.following).toEqual([]);
83 });
84
85 test('returns list as JSON', () => {
86 const list = [
87 { handle: 'alice.bsky.social', did: 'did:plc:alice', followedAt: '2026-01-01T00:00:00Z' },
88 ];
89 writeFileSync(join(tmpDir, '.vit', 'following.json'), JSON.stringify(list));
90 const r = run('following --json', tmpDir);
91 const j = parseJson(r.stdout);
92 expect(j.ok).toBe(true);
93 expect(j.following).toHaveLength(1);
94 expect(j.following[0].handle).toBe('alice.bsky.social');
95 });
96 });
97
98 describe('unfollow --json', () => {
99 test('returns error when not following', () => {
100 writeFileSync(join(tmpDir, '.vit', 'following.json'), '[]');
101 const r = run('unfollow nobody.bsky.social --json', tmpDir);
102 expect(r.exitCode).toBe(1);
103 const j = parseJson(r.stdout);
104 expect(j.ok).toBe(false);
105 expect(j.error).toContain('not following');
106 });
107
108 test('returns success JSON on unfollow', () => {
109 const list = [{ handle: 'alice.bsky.social', did: 'did:plc:alice', followedAt: '2026-01-01T00:00:00Z' }];
110 writeFileSync(join(tmpDir, '.vit', 'following.json'), JSON.stringify(list));
111 const r = run('unfollow alice.bsky.social --json', tmpDir);
112 expect(r.exitCode).toBe(0);
113 const j = parseJson(r.stdout);
114 expect(j.ok).toBe(true);
115 expect(j.handle).toBe('alice.bsky.social');
116 });
117 });
118
119 describe('follow --json', () => {
120 test('returns error when no DID configured', () => {
121 const configHome = join(tmpdir(), '.test-json-follow-' + Math.random().toString(36).slice(2));
122 mkdirSync(configHome, { recursive: true });
123 const r = run('follow someone.bsky.social --json', tmpDir, {
124 CLAUDECODE: '',
125 GEMINI_CLI: '',
126 CODEX_CI: '',
127 XDG_CONFIG_HOME: configHome,
128 });
129 expect(r.exitCode).toBe(1);
130 const j = parseJson(r.stdout);
131 expect(j.ok).toBe(false);
132 expect(j.error).toContain('no DID configured');
133 rmSync(configHome, { recursive: true, force: true });
134 });
135 });
136
137 describe('ship --json', () => {
138 test('missing --title returns JSON error', () => {
139 const r = run('ship --json --description "desc" --ref "one-two-three"');
140 const j = parseJson(r.stdout);
141 expect(j.ok).toBe(false);
142 expect(j.error).toContain('--title');
143 });
144
145 test('missing --description returns JSON error', () => {
146 const r = run('ship --json --title "Hi" --ref "one-two-three"');
147 const j = parseJson(r.stdout);
148 expect(j.ok).toBe(false);
149 expect(j.error).toContain('--description');
150 });
151
152 test('missing --ref returns JSON error', () => {
153 const r = run('ship --json --title "Hi" --description "desc"');
154 const j = parseJson(r.stdout);
155 expect(j.ok).toBe(false);
156 expect(j.error).toContain('--ref');
157 });
158
159 test('non-agent returns JSON error', () => {
160 const r = run('ship --json --title "Hi" --description "desc" --ref "one-two-three"', '/tmp', { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' }, 'body');
161 const j = parseJson(r.stdout);
162 expect(j.ok).toBe(false);
163 expect(j.error).toContain('agent required');
164 });
165
166 test('empty body returns JSON error', () => {
167 const r = run('ship --json --title "Hi" --description "desc" --ref "one-two-three" --did "did:plc:abc"', undefined, agentEnv, '');
168 const j = parseJson(r.stdout);
169 expect(j.ok).toBe(false);
170 expect(j.error).toContain('body is required');
171 });
172
173 test('invalid ref returns JSON error', () => {
174 const r = run('ship --json --title "Hi" --description "desc" --ref "Bad-Ref" --did "did:plc:abc"', undefined, agentEnv, 'body');
175 const j = parseJson(r.stdout);
176 expect(j.ok).toBe(false);
177 expect(j.error).toContain('three lowercase words');
178 });
179
180 test('invalid kind returns JSON error', () => {
181 const r = run('ship --json --title "Hi" --description "desc" --ref "one-two-three" --kind "invalid" --did "did:plc:abc"', undefined, agentEnv, 'body');
182 const j = parseJson(r.stdout);
183 expect(j.ok).toBe(false);
184 expect(j.error).toContain('--kind');
185 });
186 });
187
188 describe('vet --json', () => {
189 test('missing ref returns JSON error', () => {
190 const r = run('vet --json', tmpDir);
191 const j = parseJson(r.stdout);
192 expect(j.ok).toBe(false);
193 expect(j.error).toContain('ref argument is required');
194 });
195
196 test('invalid ref returns JSON error', () => {
197 const r = run('vet BADREF --json', tmpDir);
198 const j = parseJson(r.stdout);
199 expect(j.ok).toBe(false);
200 expect(j.error).toContain('invalid ref');
201 });
202 });
203
204 describe('vouch --json', () => {
205 test('invalid ref returns JSON error', () => {
206 const r = run('vouch BADREF --json', tmpDir);
207 const j = parseJson(r.stdout);
208 expect(j.ok).toBe(false);
209 expect(j.error).toContain('invalid ref');
210 });
211 });
212
213 describe('remix --json', () => {
214 test('invalid ref returns JSON error', () => {
215 const r = run('remix BADREF --json', tmpDir, agentEnv);
216 const j = parseJson(r.stdout);
217 expect(j.ok).toBe(false);
218 expect(j.error).toContain('invalid ref');
219 });
220
221 test('non-agent returns JSON error', () => {
222 const r = run('remix one-two-three --json', tmpDir, { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' });
223 const j = parseJson(r.stdout);
224 expect(j.ok).toBe(false);
225 expect(j.error).toContain('agent required');
226 });
227 });
228
229 describe('learn --json', () => {
230 test('invalid ref returns JSON error', () => {
231 const r = run('learn badref --json', tmpDir, agentEnv);
232 const j = parseJson(r.stdout);
233 expect(j.ok).toBe(false);
234 expect(j.error).toContain('invalid skill ref');
235 });
236
237 test('non-agent returns JSON error', () => {
238 const r = run('learn skill-test --json', tmpDir, { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' });
239 const j = parseJson(r.stdout);
240 expect(j.ok).toBe(false);
241 expect(j.error).toContain('agent required');
242 });
243 });
244
245 describe('scan --json', () => {
246 test('invalid --days returns JSON error', () => {
247 const r = run('scan --json --days 0', tmpDir);
248 const j = parseJson(r.stdout);
249 expect(j.ok).toBe(false);
250 expect(j.error).toContain('--days must be a positive integer');
251 });
252 });
253});