open source is social v-it.org
at main 253 lines 9.0 kB view raw
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});