open source is social v-it.org
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix(test): sandbox all test filesystem and network access

- link: override HOME to temp dir instead of writing to ~/.local/bin
- beacon: mock isomorphic-git in-process instead of live GitHub clones
- explore: route 12 tests through local Bun.serve() mock server
- doctor: isolate with HOME + XDG_CONFIG_HOME env overrides
- trust-gate, vit-dir: add optional dir param to vitDir() and
dependents, eliminating process.chdir() from tests
- CLAUDE.md: add Testing Standards section (5 rules)

342/342 tests pass. No external HTTP, no real home access,
no process.chdir() in tests.

+293 -89
+8
CLAUDE.md
··· 31 31 - **Fail fast** - Validate inputs and external state early. Clear error messages. 32 32 - **Vocabulary alignment** - `VOCAB.md` is the source of truth for all project terminology. All project descriptions, CLI help strings, documentation, and skill files must use terminology consistent with VOCAB.md. When VOCAB.md is updated, propagate changes to every file that references vit's vocabulary in the same commit. 33 33 34 + ## Testing Standards 35 + 36 + 1. **No real home access** — tests must never read or write `~/.local`, `~/.config`, `~/.claude`, or any path under the real `$HOME`. Use `HOME` and `XDG_CONFIG_HOME` env overrides to redirect to temp dirs. 37 + 2. **No live HTTP** — tests must not make requests to external hosts (`github.com`, `explore.v-it.org`, etc.). Use `Bun.serve()` local servers with canned responses, or `mock.module()` for dependencies that make HTTP calls. 38 + 3. **No global mutation** — tests must not call `process.chdir()`. Pass directory arguments to functions instead. If `process.chdir` is unavoidable, wrap in `try/finally`. 39 + 4. **Temp dir lifecycle** — create temp dirs with `join(tmpdir(), '.test-{name}-{random}')` in `beforeEach`, clean up with `rmSync(dir, { recursive: true, force: true })` in `afterEach`. 40 + 5. **Subprocess isolation** — tests using `run()` isolate via env overrides and CLI flags. In-process tests may use `mock.module()` only when env/flag isolation is impossible (e.g., mocking `isomorphic-git` in beacon tests). 41 + 34 42 ## Verification 35 43 36 44 - Always run `make test` before committing — all tests must pass.
+4 -4
src/lib/trust-gate.js
··· 12 12 * Returns { accepted: true } or { accepted: false }. 13 13 * No TTL — once set, it's permanent until deleted. 14 14 */ 15 - export function checkDangerousAccept() { 16 - const p = join(vitDir(), ACCEPT_FILE); 15 + export function checkDangerousAccept(dir) { 16 + const p = join(vitDir(dir), ACCEPT_FILE); 17 17 if (!existsSync(p)) return { accepted: false }; 18 18 try { 19 19 JSON.parse(readFileSync(p, 'utf-8')); ··· 30 30 * Bypass condition: dangerous-accept flag is active. 31 31 * Caller checks trusted.jsonl before calling this. 32 32 */ 33 - export function shouldBypassVet() { 34 - const accept = checkDangerousAccept(); 33 + export function shouldBypassVet(dir) { 34 + const accept = checkDangerousAccept(dir); 35 35 if (accept.accepted) { 36 36 return { bypass: true, reason: 'dangerous-accept' }; 37 37 }
+18 -18
src/lib/vit-dir.js
··· 4 4 import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs'; 5 5 import { join } from 'node:path'; 6 6 7 - export function vitDir() { 8 - return join(process.cwd(), '.vit'); 7 + export function vitDir(dir) { 8 + return join(dir || process.cwd(), '.vit'); 9 9 } 10 10 11 - export function readProjectConfig() { 12 - const p = join(vitDir(), 'config.json'); 11 + export function readProjectConfig(dir) { 12 + const p = join(vitDir(dir), 'config.json'); 13 13 if (!existsSync(p)) return {}; 14 14 try { 15 15 return JSON.parse(readFileSync(p, 'utf-8')); ··· 18 18 } 19 19 } 20 20 21 - export function readBeaconSet() { 22 - const config = readProjectConfig(); 21 + export function readBeaconSet(dir) { 22 + const config = readProjectConfig(dir); 23 23 const set = new Set(); 24 24 if (config.beacon) set.add(config.beacon); 25 25 if (config.secondaryBeacon) set.add(config.secondaryBeacon); ··· 32 32 writeFileSync(join(dir, 'config.json'), JSON.stringify(obj, null, 2) + '\n'); 33 33 } 34 34 35 - export function appendLog(filename, record) { 36 - const dir = vitDir(); 37 - mkdirSync(dir, { recursive: true }); 38 - appendFileSync(join(dir, filename), JSON.stringify(record) + '\n'); 35 + export function appendLog(filename, record, dir) { 36 + const d = vitDir(dir); 37 + mkdirSync(d, { recursive: true }); 38 + appendFileSync(join(d, filename), JSON.stringify(record) + '\n'); 39 39 } 40 40 41 - export function readLog(filename) { 42 - const p = join(vitDir(), filename); 41 + export function readLog(filename, dir) { 42 + const p = join(vitDir(dir), filename); 43 43 if (!existsSync(p)) return []; 44 44 try { 45 45 return readFileSync(p, 'utf-8') ··· 52 52 } 53 53 } 54 54 55 - export function readFollowing() { 56 - const p = join(vitDir(), 'following.json'); 55 + export function readFollowing(dir) { 56 + const p = join(vitDir(dir), 'following.json'); 57 57 if (!existsSync(p)) return []; 58 58 try { 59 59 return JSON.parse(readFileSync(p, 'utf-8')); ··· 62 62 } 63 63 } 64 64 65 - export function writeFollowing(list) { 66 - const dir = vitDir(); 67 - mkdirSync(dir, { recursive: true }); 68 - writeFileSync(join(dir, 'following.json'), JSON.stringify(list, null, 2) + '\n'); 65 + export function writeFollowing(list, dir) { 66 + const d = vitDir(dir); 67 + mkdirSync(d, { recursive: true }); 68 + writeFileSync(join(d, 'following.json'), JSON.stringify(list, null, 2) + '\n'); 69 69 }
+75 -17
test/beacon-cmd.test.js
··· 1 1 // SPDX-License-Identifier: MIT 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { describe, test, expect } from 'bun:test'; 5 - import { run } from './helpers.js'; 4 + import { describe, test, expect, mock, spyOn, beforeEach, afterEach, afterAll } from 'bun:test'; 5 + import { Command } from 'commander'; 6 + 7 + const FAKE_HEAD = 'abc123def456'; 8 + const FAKE_TREE = 'tree789xyz'; 9 + 10 + mock.module('isomorphic-git', () => ({ 11 + default: { 12 + clone: async () => {}, 13 + resolveRef: async () => FAKE_HEAD, 14 + readObject: async ({ oid }) => { 15 + if (oid === FAKE_HEAD) { 16 + return { object: { tree: FAKE_TREE } }; 17 + } 18 + return { object: [] }; 19 + }, 20 + }, 21 + })); 22 + 23 + const { default: registerBeacon } = await import('../src/cmd/beacon.js'); 6 24 7 25 describe('vit beacon', () => { 8 - test('probes a public repo without .vit/config.json beacon (unlit)', () => { 9 - const result = run('beacon https://github.com/octocat/Hello-World.git'); 10 - expect(result.exitCode).toBe(0); 11 - expect(result.stdout).toContain('beacon: unlit'); 12 - }, 30000); 26 + let logSpy; 27 + let errorSpy; 28 + let savedExitCode; 13 29 14 - test('errors on invalid URL', () => { 15 - const result = run('beacon notaurl'); 16 - expect(result.exitCode).not.toBe(0); 17 - expect(result.stderr).toBeTruthy(); 18 - }, 30000); 30 + beforeEach(() => { 31 + savedExitCode = process.exitCode; 32 + process.exitCode = undefined; 33 + logSpy = spyOn(console, 'log').mockImplementation(() => {}); 34 + errorSpy = spyOn(console, 'error').mockImplementation(() => {}); 35 + }); 19 36 20 - test('errors on nonexistent repo', () => { 21 - const result = run('beacon https://github.com/nonexistent-user-abc/repo404.git'); 22 - expect(result.exitCode).not.toBe(0); 23 - expect(result.stderr).toBeTruthy(); 24 - }, 30000); 37 + afterEach(() => { 38 + logSpy.mockRestore(); 39 + errorSpy.mockRestore(); 40 + process.exitCode = savedExitCode ?? 0; 41 + }); 42 + 43 + afterAll(() => { 44 + mock.restore(); 45 + }); 46 + 47 + test('probes a public repo without .vit/config.json beacon (unlit)', async () => { 48 + const program = new Command(); 49 + program.exitOverride(); 50 + registerBeacon(program); 51 + await program.parseAsync(['beacon', 'https://github.com/octocat/Hello-World.git'], { from: 'user' }); 52 + const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n'); 53 + expect(output).toContain('beacon: unlit'); 54 + }); 55 + 56 + test('errors on invalid URL', async () => { 57 + const program = new Command(); 58 + program.exitOverride(); 59 + registerBeacon(program); 60 + await program.parseAsync(['beacon', 'notaurl'], { from: 'user' }); 61 + expect(process.exitCode).toBe(1); 62 + expect(errorSpy).toHaveBeenCalled(); 63 + }); 64 + 65 + test('errors on nonexistent repo', async () => { 66 + const gitMod = await import('isomorphic-git'); 67 + const origClone = gitMod.default.clone; 68 + try { 69 + gitMod.default.clone = async () => { 70 + throw new Error('remote: Repository not found'); 71 + }; 72 + 73 + const program = new Command(); 74 + program.exitOverride(); 75 + registerBeacon(program); 76 + await program.parseAsync(['beacon', 'https://github.com/nonexistent-user-abc/repo404.git'], { from: 'user' }); 77 + expect(process.exitCode).toBe(1); 78 + expect(errorSpy).toHaveBeenCalled(); 79 + } finally { 80 + gitMod.default.clone = origClone; 81 + } 82 + }); 25 83 });
+21 -6
test/doctor.test.js
··· 1 1 // SPDX-License-Identifier: MIT 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { describe, test, expect } from 'bun:test'; 4 + import { describe, test, expect, afterEach } from 'bun:test'; 5 5 import { run } from './helpers.js'; 6 + import { mkdirSync, rmSync } from 'node:fs'; 7 + import { tmpdir } from 'node:os'; 8 + import { join } from 'node:path'; 6 9 7 10 describe('vit doctor', () => { 11 + let tmpHome; 12 + 13 + afterEach(() => { 14 + if (tmpHome) rmSync(tmpHome, { recursive: true, force: true }); 15 + }); 16 + 17 + function doctorEnv() { 18 + tmpHome = join(tmpdir(), '.test-doctor-' + Math.random().toString(36).slice(2)); 19 + mkdirSync(tmpHome, { recursive: true }); 20 + return { HOME: tmpHome, XDG_CONFIG_HOME: join(tmpHome, '.config') }; 21 + } 22 + 8 23 test('reports beacon status', () => { 9 - const result = run('doctor'); 24 + const result = run('doctor', undefined, doctorEnv()); 10 25 expect(result.stdout).toMatch(/beacon:/); 11 26 }); 12 27 13 28 test('reports skill status', () => { 14 - const result = run('doctor'); 29 + const result = run('doctor', undefined, doctorEnv()); 15 30 expect(result.stdout).toMatch(/skill:/); 16 31 }); 17 32 18 33 test('reports bluesky status', () => { 19 - const result = run('doctor'); 34 + const result = run('doctor', undefined, doctorEnv()); 20 35 expect(result.stdout).toMatch(/bluesky:/); 21 36 }); 22 37 23 38 test('reports install type', () => { 24 - const result = run('doctor'); 39 + const result = run('doctor', undefined, doctorEnv()); 25 40 expect(result.stdout).toMatch(/install:/); 26 41 }); 27 42 28 43 test('vit status is an alias for doctor', () => { 29 - const result = run('status'); 44 + const result = run('status', undefined, doctorEnv()); 30 45 expect(result.stdout).toMatch(/install:/); 31 46 }); 32 47 });
+129 -13
test/explore.test.js
··· 1 1 // SPDX-License-Identifier: MIT 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { describe, test, expect } from 'bun:test'; 4 + import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; 5 + import { spawn } from 'node:child_process'; 5 6 import { run } from './helpers.js'; 6 7 8 + let server; 9 + let port; 10 + 11 + beforeAll(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 + 112 + afterAll(async () => { 113 + if (!server || server.exitCode !== null) return; 114 + await new Promise((resolve) => { 115 + server.once('exit', () => resolve()); 116 + server.kill(); 117 + }); 118 + }); 119 + 7 120 describe('vit explore', () => { 8 121 test('shows help', () => { 9 122 const result = run('explore --help', '/tmp'); ··· 12 125 }); 13 126 14 127 test('stats returns JSON', () => { 15 - const result = run('explore stats --json', '/tmp'); 128 + const result = run(`explore stats --json --explore-url http://localhost:${port}`, '/tmp'); 16 129 expect(result.exitCode).toBe(0); 17 130 const data = JSON.parse(result.stdout); 18 131 expect(data.ok).toBe(true); ··· 20 133 }); 21 134 22 135 test('caps returns JSON', () => { 23 - const result = run('explore caps --json --limit 2', '/tmp'); 136 + const result = run(`explore caps --json --limit 2 --explore-url http://localhost:${port}`, '/tmp'); 24 137 expect(result.exitCode).toBe(0); 25 138 const data = JSON.parse(result.stdout); 26 139 expect(data.ok).toBe(true); ··· 28 141 }); 29 142 30 143 test('skills returns JSON', () => { 31 - const result = run('explore skills --json --limit 2', '/tmp'); 144 + const result = run(`explore skills --json --limit 2 --explore-url http://localhost:${port}`, '/tmp'); 32 145 expect(result.exitCode).toBe(0); 33 146 const data = JSON.parse(result.stdout); 34 147 expect(data.ok).toBe(true); ··· 36 149 }); 37 150 38 151 test('beacons returns JSON', () => { 39 - const result = run('explore beacons --json', '/tmp'); 152 + const result = run(`explore beacons --json --explore-url http://localhost:${port}`, '/tmp'); 40 153 expect(result.exitCode).toBe(0); 41 154 const data = JSON.parse(result.stdout); 42 155 expect(data.ok).toBe(true); ··· 76 189 77 190 test('flag overrides env var', () => { 78 191 const result = run( 79 - 'explore stats --json --explore-url https://explore.v-it.org', 192 + `explore stats --json --explore-url http://localhost:${port}`, 80 193 '/tmp', 81 194 { VIT_EXPLORE_URL: 'http://localhost:1' }, 82 195 ); ··· 86 199 }); 87 200 88 201 test('cap detail returns JSON', () => { 89 - const result = run('explore cap network-content-seeding --json', '/tmp'); 202 + const result = run(`explore cap network-content-seeding --json --explore-url http://localhost:${port}`, '/tmp'); 90 203 expect(result.exitCode).toBe(0); 91 204 const data = JSON.parse(result.stdout); 92 205 expect(data.ok).toBe(true); ··· 96 209 }); 97 210 98 211 test('cap detail with beacon', () => { 99 - const result = run('explore cap network-content-seeding --beacon vit:github.com/solpbc/vit --json', '/tmp'); 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 + ); 100 216 expect(result.exitCode).toBe(0); 101 217 const data = JSON.parse(result.stdout); 102 218 expect(data.ok).toBe(true); ··· 105 221 }); 106 222 107 223 test('cap not found', () => { 108 - const result = run('explore cap nonexistent-ref-xyz --json', '/tmp'); 224 + const result = run(`explore cap nonexistent-ref-xyz --json --explore-url http://localhost:${port}`, '/tmp'); 109 225 expect(result.exitCode).not.toBe(0); 110 226 const data = JSON.parse(result.stdout); 111 227 expect(data.ok).toBe(false); ··· 113 229 }); 114 230 115 231 test('skill detail returns JSON', () => { 116 - const result = run('explore skill atproto-records --json', '/tmp'); 232 + const result = run(`explore skill atproto-records --json --explore-url http://localhost:${port}`, '/tmp'); 117 233 expect(result.exitCode).toBe(0); 118 234 const data = JSON.parse(result.stdout); 119 235 expect(data.ok).toBe(true); ··· 123 239 }); 124 240 125 241 test('skill not found', () => { 126 - const result = run('explore skill nonexistent-skill-xyz --json', '/tmp'); 242 + const result = run(`explore skill nonexistent-skill-xyz --json --explore-url http://localhost:${port}`, '/tmp'); 127 243 expect(result.exitCode).not.toBe(0); 128 244 const data = JSON.parse(result.stdout); 129 245 expect(data.ok).toBe(false); ··· 131 247 }); 132 248 133 249 test('caps --kind filter passes kind to API', () => { 134 - const result = run('explore caps --kind request --json --limit 2', '/tmp'); 250 + const result = run(`explore caps --kind request --json --limit 2 --explore-url http://localhost:${port}`, '/tmp'); 135 251 expect(result.exitCode).toBe(0); 136 252 const data = JSON.parse(result.stdout); 137 253 expect(data.ok).toBe(true); ··· 147 263 }); 148 264 149 265 test('bare explore returns stats JSON', () => { 150 - const result = run('explore --json', '/tmp'); 266 + const result = run(`explore --json --explore-url http://localhost:${port}`, '/tmp'); 151 267 expect(result.exitCode).toBe(0); 152 268 const data = JSON.parse(result.stdout); 153 269 expect(data.ok).toBe(true);
+20 -4
test/link.test.js
··· 1 1 // SPDX-License-Identifier: MIT 2 2 // Copyright (c) 2026 sol pbc 3 3 4 - import { describe, test, expect } from 'bun:test'; 4 + import { describe, test, expect, afterEach } from 'bun:test'; 5 5 import { run } from './helpers.js'; 6 + import { mkdirSync, rmSync, existsSync } from 'node:fs'; 7 + import { tmpdir } from 'node:os'; 8 + import { join } from 'node:path'; 6 9 7 10 describe('vit link', () => { 11 + let tmpHome; 12 + 13 + afterEach(() => { 14 + if (tmpHome) rmSync(tmpHome, { recursive: true, force: true }); 15 + }); 16 + 8 17 test('--help shows usage', () => { 9 18 const result = run('link --help'); 10 19 expect(result.stdout).toContain('Link'); ··· 12 21 }); 13 22 14 23 test('creates symlink without error', () => { 15 - const result = run('link'); 24 + tmpHome = join(tmpdir(), '.test-link-' + Math.random().toString(36).slice(2)); 25 + mkdirSync(tmpHome, { recursive: true }); 26 + const result = run('link', undefined, { HOME: tmpHome }); 16 27 expect(result.exitCode).toBe(0); 28 + const linkPath = join(tmpHome, '.local', 'bin', 'vit'); 29 + expect(existsSync(linkPath)).toBe(true); 17 30 }); 18 31 19 32 test('is idempotent', () => { 20 - run('link'); 21 - const result = run('link'); 33 + tmpHome = join(tmpdir(), '.test-link-' + Math.random().toString(36).slice(2)); 34 + mkdirSync(tmpHome, { recursive: true }); 35 + const env = { HOME: tmpHome }; 36 + run('link', undefined, env); 37 + const result = run('link', undefined, env); 22 38 expect(result.exitCode).toBe(0); 23 39 }); 24 40 });
+6 -10
test/trust-gate.test.js
··· 8 8 9 9 describe('trust-gate', () => { 10 10 let tmp; 11 - let originalCwd; 12 11 13 12 beforeEach(() => { 14 13 tmp = join(tmpdir(), '.test-trust-gate-' + Math.random().toString(36).slice(2)); 15 14 mkdirSync(join(tmp, '.vit'), { recursive: true }); 16 - originalCwd = process.cwd(); 17 - process.chdir(tmp); 18 15 }); 19 16 20 17 afterEach(() => { 21 - process.chdir(originalCwd); 22 18 rmSync(tmp, { recursive: true, force: true }); 23 19 }); 24 20 ··· 32 28 describe('checkDangerousAccept', () => { 33 29 test('returns accepted false when no file exists', async () => { 34 30 const { checkDangerousAccept } = await loadModule(); 35 - expect(checkDangerousAccept()).toEqual({ accepted: false }); 31 + expect(checkDangerousAccept(tmp)).toEqual({ accepted: false }); 36 32 }); 37 33 38 34 test('returns accepted true when file exists with valid JSON', async () => { 39 35 const { checkDangerousAccept } = await loadModule(); 40 36 writeFileSync(join(tmp, '.vit', 'dangerous-accept'), JSON.stringify({ acceptedAt: '2026-03-26T14:30:00.000Z' })); 41 - expect(checkDangerousAccept()).toEqual({ accepted: true }); 37 + expect(checkDangerousAccept(tmp)).toEqual({ accepted: true }); 42 38 }); 43 39 44 40 test('returns accepted false when file is malformed JSON', async () => { 45 41 const { checkDangerousAccept } = await loadModule(); 46 42 writeFileSync(join(tmp, '.vit', 'dangerous-accept'), 'not json'); 47 - expect(checkDangerousAccept()).toEqual({ accepted: false }); 43 + expect(checkDangerousAccept(tmp)).toEqual({ accepted: false }); 48 44 }); 49 45 50 46 test('no TTL — old timestamps still accepted', async () => { 51 47 const { checkDangerousAccept } = await loadModule(); 52 48 writeFileSync(join(tmp, '.vit', 'dangerous-accept'), JSON.stringify({ acceptedAt: '2020-01-01T00:00:00.000Z' })); 53 - expect(checkDangerousAccept()).toEqual({ accepted: true }); 49 + expect(checkDangerousAccept(tmp)).toEqual({ accepted: true }); 54 50 }); 55 51 }); 56 52 ··· 58 54 test('returns bypass true with reason when flag active', async () => { 59 55 const { shouldBypassVet } = await loadModule(); 60 56 writeFileSync(join(tmp, '.vit', 'dangerous-accept'), JSON.stringify({ acceptedAt: '2026-03-26T14:30:00.000Z' })); 61 - expect(shouldBypassVet()).toEqual({ bypass: true, reason: 'dangerous-accept' }); 57 + expect(shouldBypassVet(tmp)).toEqual({ bypass: true, reason: 'dangerous-accept' }); 62 58 }); 63 59 64 60 test('returns bypass false when flag absent', async () => { 65 61 const { shouldBypassVet } = await loadModule(); 66 - expect(shouldBypassVet()).toEqual({ bypass: false }); 62 + expect(shouldBypassVet(tmp)).toEqual({ bypass: false }); 67 63 }); 68 64 }); 69 65 });
+12 -17
test/vit-dir.test.js
··· 6 6 import { tmpdir } from 'node:os'; 7 7 import { join } from 'node:path'; 8 8 9 - // vit-dir functions use process.cwd(), so we save/restore it 10 - const originalCwd = process.cwd(); 11 - 12 9 describe('vit-dir', () => { 13 10 let tmpDir; 14 11 15 12 beforeEach(() => { 16 13 tmpDir = join(tmpdir(), '.test-vit-dir-' + Math.random().toString(36).slice(2)); 17 14 mkdirSync(tmpDir, { recursive: true }); 18 - process.chdir(tmpDir); 19 15 }); 20 16 21 17 afterEach(() => { 22 - process.chdir(originalCwd); 23 18 rmSync(tmpDir, { recursive: true, force: true }); 24 19 }); 25 20 26 21 test('writeProjectConfig creates .vit/ and config.json', async () => { 27 22 const { writeProjectConfig } = await import('../src/lib/vit-dir.js'); 28 - writeProjectConfig({ beacon: 'vit:github.com/org/repo' }); 23 + writeProjectConfig({ beacon: 'vit:github.com/org/repo' }, tmpDir); 29 24 expect(existsSync(join(tmpDir, '.vit'))).toBe(true); 30 25 const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8'); 31 26 const parsed = JSON.parse(content); ··· 34 29 35 30 test('readProjectConfig reads written config', async () => { 36 31 const { writeProjectConfig, readProjectConfig } = await import('../src/lib/vit-dir.js'); 37 - writeProjectConfig({ beacon: 'vit:github.com/org/repo' }); 38 - const config = readProjectConfig(); 32 + writeProjectConfig({ beacon: 'vit:github.com/org/repo' }, tmpDir); 33 + const config = readProjectConfig(tmpDir); 39 34 expect(config.beacon).toBe('vit:github.com/org/repo'); 40 35 }); 41 36 42 37 test('readProjectConfig returns {} when file missing', async () => { 43 38 const { readProjectConfig } = await import('../src/lib/vit-dir.js'); 44 - const config = readProjectConfig(); 39 + const config = readProjectConfig(tmpDir); 45 40 expect(config).toEqual({}); 46 41 }); 47 42 48 43 test('readBeaconSet returns empty Set when no config', async () => { 49 44 const { readBeaconSet } = await import('../src/lib/vit-dir.js'); 50 - const set = readBeaconSet(); 45 + const set = readBeaconSet(tmpDir); 51 46 expect(set).toBeInstanceOf(Set); 52 47 expect(set.size).toBe(0); 53 48 }); 54 49 55 50 test('readBeaconSet returns primary only when no secondary', async () => { 56 51 const { writeProjectConfig, readBeaconSet } = await import('../src/lib/vit-dir.js'); 57 - writeProjectConfig({ beacon: 'vit:github.com/org/repo' }); 58 - const set = readBeaconSet(); 52 + writeProjectConfig({ beacon: 'vit:github.com/org/repo' }, tmpDir); 53 + const set = readBeaconSet(tmpDir); 59 54 expect(set.size).toBe(1); 60 55 expect(set.has('vit:github.com/org/repo')).toBe(true); 61 56 }); 62 57 63 58 test('readBeaconSet returns both when secondary is set', async () => { 64 59 const { writeProjectConfig, readBeaconSet } = await import('../src/lib/vit-dir.js'); 65 - writeProjectConfig({ beacon: 'vit:github.com/org/repo', secondaryBeacon: 'vit:github.com/upstream/repo' }); 66 - const set = readBeaconSet(); 60 + writeProjectConfig({ beacon: 'vit:github.com/org/repo', secondaryBeacon: 'vit:github.com/upstream/repo' }, tmpDir); 61 + const set = readBeaconSet(tmpDir); 67 62 expect(set.size).toBe(2); 68 63 expect(set.has('vit:github.com/org/repo')).toBe(true); 69 64 expect(set.has('vit:github.com/upstream/repo')).toBe(true); ··· 71 66 72 67 test('appendLog creates file and appends JSONL line', async () => { 73 68 const { appendLog } = await import('../src/lib/vit-dir.js'); 74 - appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', did: 'did:plc:test' }); 69 + appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', did: 'did:plc:test' }, tmpDir); 75 70 const content = readFileSync(join(tmpDir, '.vit', 'caps.jsonl'), 'utf-8'); 76 71 const lines = content.trim().split('\n'); 77 72 expect(lines.length).toBe(1); ··· 80 75 81 76 test('appendLog appends to existing file', async () => { 82 77 const { appendLog } = await import('../src/lib/vit-dir.js'); 83 - appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', n: 1 }); 84 - appendLog('caps.jsonl', { ts: '2026-01-02T00:00:00Z', n: 2 }); 78 + appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', n: 1 }, tmpDir); 79 + appendLog('caps.jsonl', { ts: '2026-01-02T00:00:00Z', n: 2 }, tmpDir); 85 80 const content = readFileSync(join(tmpDir, '.vit', 'caps.jsonl'), 'utf-8'); 86 81 const lines = content.trim().split('\n'); 87 82 expect(lines.length).toBe(2);