From db4fe4b92a8af2296b2ebbbeff3de33deb56098b Mon Sep 17 00:00:00 2001 From: Jer Miller Date: Wed, 15 Apr 2026 21:49:30 -0600 Subject: [PATCH] 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. --- CLAUDE.md | 8 +++ src/lib/trust-gate.js | 8 +-- src/lib/vit-dir.js | 36 +++++----- test/beacon-cmd.test.js | 96 +++++++++++++++++++++------ test/doctor.test.js | 27 ++++++-- test/explore.test.js | 142 ++++++++++++++++++++++++++++++++++++---- test/link.test.js | 24 +++++-- test/trust-gate.test.js | 16 ++--- test/vit-dir.test.js | 29 ++++---- 9 files changed, 295 insertions(+), 91 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b74f383..34482bb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,6 +31,14 @@ make clean # Remove node_modules - **Fail fast** - Validate inputs and external state early. Clear error messages. - **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. +## Testing Standards + +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. +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. +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`. +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`. +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). + ## Verification - Always run `make test` before committing — all tests must pass. diff --git a/src/lib/trust-gate.js b/src/lib/trust-gate.js index e7db36d..b525d81 100644 --- a/src/lib/trust-gate.js +++ b/src/lib/trust-gate.js @@ -12,8 +12,8 @@ const ACCEPT_FILE = 'dangerous-accept'; * Returns { accepted: true } or { accepted: false }. * No TTL — once set, it's permanent until deleted. */ -export function checkDangerousAccept() { - const p = join(vitDir(), ACCEPT_FILE); +export function checkDangerousAccept(dir) { + const p = join(vitDir(dir), ACCEPT_FILE); if (!existsSync(p)) return { accepted: false }; try { JSON.parse(readFileSync(p, 'utf-8')); @@ -30,8 +30,8 @@ export function checkDangerousAccept() { * Bypass condition: dangerous-accept flag is active. * Caller checks trusted.jsonl before calling this. */ -export function shouldBypassVet() { - const accept = checkDangerousAccept(); +export function shouldBypassVet(dir) { + const accept = checkDangerousAccept(dir); if (accept.accepted) { return { bypass: true, reason: 'dangerous-accept' }; } diff --git a/src/lib/vit-dir.js b/src/lib/vit-dir.js index 093ffd3..5e99bac 100644 --- a/src/lib/vit-dir.js +++ b/src/lib/vit-dir.js @@ -4,12 +4,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync } from 'node:fs'; import { join } from 'node:path'; -export function vitDir() { - return join(process.cwd(), '.vit'); +export function vitDir(dir) { + return join(dir || process.cwd(), '.vit'); } -export function readProjectConfig() { - const p = join(vitDir(), 'config.json'); +export function readProjectConfig(dir) { + const p = join(vitDir(dir), 'config.json'); if (!existsSync(p)) return {}; try { return JSON.parse(readFileSync(p, 'utf-8')); @@ -18,8 +18,8 @@ export function readProjectConfig() { } } -export function readBeaconSet() { - const config = readProjectConfig(); +export function readBeaconSet(dir) { + const config = readProjectConfig(dir); const set = new Set(); if (config.beacon) set.add(config.beacon); if (config.secondaryBeacon) set.add(config.secondaryBeacon); @@ -32,14 +32,14 @@ export function writeProjectConfig(obj, baseDir) { writeFileSync(join(dir, 'config.json'), JSON.stringify(obj, null, 2) + '\n'); } -export function appendLog(filename, record) { - const dir = vitDir(); - mkdirSync(dir, { recursive: true }); - appendFileSync(join(dir, filename), JSON.stringify(record) + '\n'); +export function appendLog(filename, record, dir) { + const d = vitDir(dir); + mkdirSync(d, { recursive: true }); + appendFileSync(join(d, filename), JSON.stringify(record) + '\n'); } -export function readLog(filename) { - const p = join(vitDir(), filename); +export function readLog(filename, dir) { + const p = join(vitDir(dir), filename); if (!existsSync(p)) return []; try { return readFileSync(p, 'utf-8') @@ -52,8 +52,8 @@ export function readLog(filename) { } } -export function readFollowing() { - const p = join(vitDir(), 'following.json'); +export function readFollowing(dir) { + const p = join(vitDir(dir), 'following.json'); if (!existsSync(p)) return []; try { return JSON.parse(readFileSync(p, 'utf-8')); @@ -62,8 +62,8 @@ export function readFollowing() { } } -export function writeFollowing(list) { - const dir = vitDir(); - mkdirSync(dir, { recursive: true }); - writeFileSync(join(dir, 'following.json'), JSON.stringify(list, null, 2) + '\n'); +export function writeFollowing(list, dir) { + const d = vitDir(dir); + mkdirSync(d, { recursive: true }); + writeFileSync(join(d, 'following.json'), JSON.stringify(list, null, 2) + '\n'); } diff --git a/test/beacon-cmd.test.js b/test/beacon-cmd.test.js index 0cdee82..b0c9e32 100644 --- a/test/beacon-cmd.test.js +++ b/test/beacon-cmd.test.js @@ -1,25 +1,83 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2026 sol pbc -import { describe, test, expect } from 'bun:test'; -import { run } from './helpers.js'; +import { describe, test, expect, mock, spyOn, beforeEach, afterEach, afterAll } from 'bun:test'; +import { Command } from 'commander'; + +const FAKE_HEAD = 'abc123def456'; +const FAKE_TREE = 'tree789xyz'; + +mock.module('isomorphic-git', () => ({ + default: { + clone: async () => {}, + resolveRef: async () => FAKE_HEAD, + readObject: async ({ oid }) => { + if (oid === FAKE_HEAD) { + return { object: { tree: FAKE_TREE } }; + } + return { object: [] }; + }, + }, +})); + +const { default: registerBeacon } = await import('../src/cmd/beacon.js'); describe('vit beacon', () => { - test('probes a public repo without .vit/config.json beacon (unlit)', () => { - const result = run('beacon https://github.com/octocat/Hello-World.git'); - expect(result.exitCode).toBe(0); - expect(result.stdout).toContain('beacon: unlit'); - }, 30000); - - test('errors on invalid URL', () => { - const result = run('beacon notaurl'); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toBeTruthy(); - }, 30000); - - test('errors on nonexistent repo', () => { - const result = run('beacon https://github.com/nonexistent-user-abc/repo404.git'); - expect(result.exitCode).not.toBe(0); - expect(result.stderr).toBeTruthy(); - }, 30000); + let logSpy; + let errorSpy; + let savedExitCode; + + beforeEach(() => { + savedExitCode = process.exitCode; + process.exitCode = undefined; + logSpy = spyOn(console, 'log').mockImplementation(() => {}); + errorSpy = spyOn(console, 'error').mockImplementation(() => {}); + }); + + afterEach(() => { + logSpy.mockRestore(); + errorSpy.mockRestore(); + process.exitCode = savedExitCode ?? 0; + }); + + afterAll(() => { + mock.restore(); + }); + + test('probes a public repo without .vit/config.json beacon (unlit)', async () => { + const program = new Command(); + program.exitOverride(); + registerBeacon(program); + await program.parseAsync(['beacon', 'https://github.com/octocat/Hello-World.git'], { from: 'user' }); + const output = logSpy.mock.calls.map(c => c.join(' ')).join('\n'); + expect(output).toContain('beacon: unlit'); + }); + + test('errors on invalid URL', async () => { + const program = new Command(); + program.exitOverride(); + registerBeacon(program); + await program.parseAsync(['beacon', 'notaurl'], { from: 'user' }); + expect(process.exitCode).toBe(1); + expect(errorSpy).toHaveBeenCalled(); + }); + + test('errors on nonexistent repo', async () => { + const gitMod = await import('isomorphic-git'); + const origClone = gitMod.default.clone; + try { + gitMod.default.clone = async () => { + throw new Error('remote: Repository not found'); + }; + + const program = new Command(); + program.exitOverride(); + registerBeacon(program); + await program.parseAsync(['beacon', 'https://github.com/nonexistent-user-abc/repo404.git'], { from: 'user' }); + expect(process.exitCode).toBe(1); + expect(errorSpy).toHaveBeenCalled(); + } finally { + gitMod.default.clone = origClone; + } + }); }); diff --git a/test/doctor.test.js b/test/doctor.test.js index 69b8ad9..8879506 100644 --- a/test/doctor.test.js +++ b/test/doctor.test.js @@ -1,32 +1,47 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2026 sol pbc -import { describe, test, expect } from 'bun:test'; +import { describe, test, expect, afterEach } from 'bun:test'; import { run } from './helpers.js'; +import { mkdirSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; describe('vit doctor', () => { + let tmpHome; + + afterEach(() => { + if (tmpHome) rmSync(tmpHome, { recursive: true, force: true }); + }); + + function doctorEnv() { + tmpHome = join(tmpdir(), '.test-doctor-' + Math.random().toString(36).slice(2)); + mkdirSync(tmpHome, { recursive: true }); + return { HOME: tmpHome, XDG_CONFIG_HOME: join(tmpHome, '.config') }; + } + test('reports beacon status', () => { - const result = run('doctor'); + const result = run('doctor', undefined, doctorEnv()); expect(result.stdout).toMatch(/beacon:/); }); test('reports skill status', () => { - const result = run('doctor'); + const result = run('doctor', undefined, doctorEnv()); expect(result.stdout).toMatch(/skill:/); }); test('reports bluesky status', () => { - const result = run('doctor'); + const result = run('doctor', undefined, doctorEnv()); expect(result.stdout).toMatch(/bluesky:/); }); test('reports install type', () => { - const result = run('doctor'); + const result = run('doctor', undefined, doctorEnv()); expect(result.stdout).toMatch(/install:/); }); test('vit status is an alias for doctor', () => { - const result = run('status'); + const result = run('status', undefined, doctorEnv()); expect(result.stdout).toMatch(/install:/); }); }); diff --git a/test/explore.test.js b/test/explore.test.js index 44bf6d0..0af82bd 100644 --- a/test/explore.test.js +++ b/test/explore.test.js @@ -1,9 +1,122 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2026 sol pbc -import { describe, test, expect } from 'bun:test'; +import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; +import { spawn } from 'node:child_process'; import { run } from './helpers.js'; +let server; +let port; + +beforeAll(async () => { + const serverScript = ` + const server = Bun.serve({ + port: 0, + fetch(req) { + const url = new URL(req.url); + const path = url.pathname; + + if (path === '/api/stats') { + return Response.json({ + total_caps: 42, total_skills: 10, total_vouches: 5, + total_beacons: 3, active_dids: 8, skill_publishers: 4, + }); + } + + if (path === '/api/caps') { + return Response.json({ + caps: [{ title: 'Test Cap', ref: 'test-cap', handle: 'test.bsky.social', + beacon: 'vit:github.com/test/repo', description: 'A test cap' }], + }); + } + + if (path === '/api/skills') { + return Response.json({ + skills: [{ name: 'test-skill', version: '1.0.0', description: 'A test skill', + handle: 'test.bsky.social' }], + }); + } + + if (path === '/api/beacons') { + return Response.json({ + beacons: [{ beacon: 'vit:github.com/test/repo', handle: 'test.bsky.social' }], + }); + } + + if (path === '/api/cap') { + const ref = url.searchParams.get('ref'); + if (ref === 'network-content-seeding') { + return Response.json({ + cap: { + ref: 'network-content-seeding', title: 'Network Content Seeding', + beacon: 'vit:github.com/solpbc/vit', handle: 'test.bsky.social', + description: 'Seed content across the network', + record_json: JSON.stringify({ kind: 'feat', text: 'test body' }), + created_at: '2026-01-01T00:00:00Z', vouch_count: 0, + }, + }); + } + return Response.json({}); + } + + if (path === '/api/skill') { + const name = url.searchParams.get('name'); + if (name === 'atproto-records') { + return Response.json({ + skill: { + name: 'atproto-records', version: '1.0.0', + description: 'AT Protocol record helpers', + handle: 'test.bsky.social', tags: 'atproto', + record_json: JSON.stringify({ license: 'MIT' }), + vouch_count: 0, + }, + }); + } + return Response.json({}); + } + + return new Response('Not Found', { status: 404 }); + }, + }); + + console.log(server.port); + `; + + server = spawn('bun', ['-e', serverScript], { stdio: ['ignore', 'pipe', 'inherit'] }); + port = await new Promise((resolve, reject) => { + let started = false; + const timer = setTimeout(() => { + server.kill(); + reject(new Error('mock explore server failed to start')); + }, 5000); + + server.stdout.setEncoding('utf-8'); + server.stdout.on('data', (chunk) => { + if (started) return; + const value = Number(String(chunk).trim().split(/\r?\n/, 1)[0]); + if (!Number.isNaN(value) && value > 0) { + started = true; + clearTimeout(timer); + resolve(value); + } + }); + server.once('exit', (code) => { + if (!started) { + clearTimeout(timer); + reject(new Error(`mock explore server exited early: ${code}`)); + } + }); + }); +}); + +afterAll(async () => { + if (!server || server.exitCode !== null) return; + await new Promise((resolve) => { + server.once('exit', () => resolve()); + server.kill(); + }); +}); + describe('vit explore', () => { test('shows help', () => { const result = run('explore --help', '/tmp'); @@ -12,7 +125,7 @@ describe('vit explore', () => { }); test('stats returns JSON', () => { - const result = run('explore stats --json', '/tmp'); + const result = run(`explore stats --json --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(true); @@ -20,7 +133,7 @@ describe('vit explore', () => { }); test('caps returns JSON', () => { - const result = run('explore caps --json --limit 2', '/tmp'); + const result = run(`explore caps --json --limit 2 --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(true); @@ -28,7 +141,7 @@ describe('vit explore', () => { }); test('skills returns JSON', () => { - const result = run('explore skills --json --limit 2', '/tmp'); + const result = run(`explore skills --json --limit 2 --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(true); @@ -36,7 +149,7 @@ describe('vit explore', () => { }); test('beacons returns JSON', () => { - const result = run('explore beacons --json', '/tmp'); + const result = run(`explore beacons --json --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(true); @@ -76,7 +189,7 @@ describe('vit explore', () => { test('flag overrides env var', () => { const result = run( - 'explore stats --json --explore-url https://explore.v-it.org', + `explore stats --json --explore-url http://localhost:${port}`, '/tmp', { VIT_EXPLORE_URL: 'http://localhost:1' }, ); @@ -86,7 +199,7 @@ describe('vit explore', () => { }); test('cap detail returns JSON', () => { - const result = run('explore cap network-content-seeding --json', '/tmp'); + const result = run(`explore cap network-content-seeding --json --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(true); @@ -96,7 +209,10 @@ describe('vit explore', () => { }); test('cap detail with beacon', () => { - const result = run('explore cap network-content-seeding --beacon vit:github.com/solpbc/vit --json', '/tmp'); + const result = run( + `explore cap network-content-seeding --beacon vit:github.com/solpbc/vit --json --explore-url http://localhost:${port}`, + '/tmp', + ); expect(result.exitCode).toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(true); @@ -105,7 +221,7 @@ describe('vit explore', () => { }); test('cap not found', () => { - const result = run('explore cap nonexistent-ref-xyz --json', '/tmp'); + const result = run(`explore cap nonexistent-ref-xyz --json --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).not.toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(false); @@ -113,7 +229,7 @@ describe('vit explore', () => { }); test('skill detail returns JSON', () => { - const result = run('explore skill atproto-records --json', '/tmp'); + const result = run(`explore skill atproto-records --json --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(true); @@ -123,7 +239,7 @@ describe('vit explore', () => { }); test('skill not found', () => { - const result = run('explore skill nonexistent-skill-xyz --json', '/tmp'); + const result = run(`explore skill nonexistent-skill-xyz --json --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).not.toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(false); @@ -131,7 +247,7 @@ describe('vit explore', () => { }); test('caps --kind filter passes kind to API', () => { - const result = run('explore caps --kind request --json --limit 2', '/tmp'); + const result = run(`explore caps --kind request --json --limit 2 --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(true); @@ -147,7 +263,7 @@ describe('vit explore', () => { }); test('bare explore returns stats JSON', () => { - const result = run('explore --json', '/tmp'); + const result = run(`explore --json --explore-url http://localhost:${port}`, '/tmp'); expect(result.exitCode).toBe(0); const data = JSON.parse(result.stdout); expect(data.ok).toBe(true); diff --git a/test/link.test.js b/test/link.test.js index c5bfe7c..12ceae3 100644 --- a/test/link.test.js +++ b/test/link.test.js @@ -1,10 +1,19 @@ // SPDX-License-Identifier: MIT // Copyright (c) 2026 sol pbc -import { describe, test, expect } from 'bun:test'; +import { describe, test, expect, afterEach } from 'bun:test'; import { run } from './helpers.js'; +import { mkdirSync, rmSync, existsSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; describe('vit link', () => { + let tmpHome; + + afterEach(() => { + if (tmpHome) rmSync(tmpHome, { recursive: true, force: true }); + }); + test('--help shows usage', () => { const result = run('link --help'); expect(result.stdout).toContain('Link'); @@ -12,13 +21,20 @@ describe('vit link', () => { }); test('creates symlink without error', () => { - const result = run('link'); + tmpHome = join(tmpdir(), '.test-link-' + Math.random().toString(36).slice(2)); + mkdirSync(tmpHome, { recursive: true }); + const result = run('link', undefined, { HOME: tmpHome }); expect(result.exitCode).toBe(0); + const linkPath = join(tmpHome, '.local', 'bin', 'vit'); + expect(existsSync(linkPath)).toBe(true); }); test('is idempotent', () => { - run('link'); - const result = run('link'); + tmpHome = join(tmpdir(), '.test-link-' + Math.random().toString(36).slice(2)); + mkdirSync(tmpHome, { recursive: true }); + const env = { HOME: tmpHome }; + run('link', undefined, env); + const result = run('link', undefined, env); expect(result.exitCode).toBe(0); }); }); diff --git a/test/trust-gate.test.js b/test/trust-gate.test.js index fe939c2..1abbbd2 100644 --- a/test/trust-gate.test.js +++ b/test/trust-gate.test.js @@ -8,17 +8,13 @@ import { tmpdir } from 'node:os'; describe('trust-gate', () => { let tmp; - let originalCwd; beforeEach(() => { tmp = join(tmpdir(), '.test-trust-gate-' + Math.random().toString(36).slice(2)); mkdirSync(join(tmp, '.vit'), { recursive: true }); - originalCwd = process.cwd(); - process.chdir(tmp); }); afterEach(() => { - process.chdir(originalCwd); rmSync(tmp, { recursive: true, force: true }); }); @@ -32,25 +28,25 @@ describe('trust-gate', () => { describe('checkDangerousAccept', () => { test('returns accepted false when no file exists', async () => { const { checkDangerousAccept } = await loadModule(); - expect(checkDangerousAccept()).toEqual({ accepted: false }); + expect(checkDangerousAccept(tmp)).toEqual({ accepted: false }); }); test('returns accepted true when file exists with valid JSON', async () => { const { checkDangerousAccept } = await loadModule(); writeFileSync(join(tmp, '.vit', 'dangerous-accept'), JSON.stringify({ acceptedAt: '2026-03-26T14:30:00.000Z' })); - expect(checkDangerousAccept()).toEqual({ accepted: true }); + expect(checkDangerousAccept(tmp)).toEqual({ accepted: true }); }); test('returns accepted false when file is malformed JSON', async () => { const { checkDangerousAccept } = await loadModule(); writeFileSync(join(tmp, '.vit', 'dangerous-accept'), 'not json'); - expect(checkDangerousAccept()).toEqual({ accepted: false }); + expect(checkDangerousAccept(tmp)).toEqual({ accepted: false }); }); test('no TTL — old timestamps still accepted', async () => { const { checkDangerousAccept } = await loadModule(); writeFileSync(join(tmp, '.vit', 'dangerous-accept'), JSON.stringify({ acceptedAt: '2020-01-01T00:00:00.000Z' })); - expect(checkDangerousAccept()).toEqual({ accepted: true }); + expect(checkDangerousAccept(tmp)).toEqual({ accepted: true }); }); }); @@ -58,12 +54,12 @@ describe('trust-gate', () => { test('returns bypass true with reason when flag active', async () => { const { shouldBypassVet } = await loadModule(); writeFileSync(join(tmp, '.vit', 'dangerous-accept'), JSON.stringify({ acceptedAt: '2026-03-26T14:30:00.000Z' })); - expect(shouldBypassVet()).toEqual({ bypass: true, reason: 'dangerous-accept' }); + expect(shouldBypassVet(tmp)).toEqual({ bypass: true, reason: 'dangerous-accept' }); }); test('returns bypass false when flag absent', async () => { const { shouldBypassVet } = await loadModule(); - expect(shouldBypassVet()).toEqual({ bypass: false }); + expect(shouldBypassVet(tmp)).toEqual({ bypass: false }); }); }); }); diff --git a/test/vit-dir.test.js b/test/vit-dir.test.js index 1080220..8cd7018 100644 --- a/test/vit-dir.test.js +++ b/test/vit-dir.test.js @@ -6,26 +6,21 @@ import { mkdirSync, rmSync, readFileSync, existsSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; -// vit-dir functions use process.cwd(), so we save/restore it -const originalCwd = process.cwd(); - describe('vit-dir', () => { let tmpDir; beforeEach(() => { tmpDir = join(tmpdir(), '.test-vit-dir-' + Math.random().toString(36).slice(2)); mkdirSync(tmpDir, { recursive: true }); - process.chdir(tmpDir); }); afterEach(() => { - process.chdir(originalCwd); rmSync(tmpDir, { recursive: true, force: true }); }); test('writeProjectConfig creates .vit/ and config.json', async () => { const { writeProjectConfig } = await import('../src/lib/vit-dir.js'); - writeProjectConfig({ beacon: 'vit:github.com/org/repo' }); + writeProjectConfig({ beacon: 'vit:github.com/org/repo' }, tmpDir); expect(existsSync(join(tmpDir, '.vit'))).toBe(true); const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8'); const parsed = JSON.parse(content); @@ -34,36 +29,36 @@ describe('vit-dir', () => { test('readProjectConfig reads written config', async () => { const { writeProjectConfig, readProjectConfig } = await import('../src/lib/vit-dir.js'); - writeProjectConfig({ beacon: 'vit:github.com/org/repo' }); - const config = readProjectConfig(); + writeProjectConfig({ beacon: 'vit:github.com/org/repo' }, tmpDir); + const config = readProjectConfig(tmpDir); expect(config.beacon).toBe('vit:github.com/org/repo'); }); test('readProjectConfig returns {} when file missing', async () => { const { readProjectConfig } = await import('../src/lib/vit-dir.js'); - const config = readProjectConfig(); + const config = readProjectConfig(tmpDir); expect(config).toEqual({}); }); test('readBeaconSet returns empty Set when no config', async () => { const { readBeaconSet } = await import('../src/lib/vit-dir.js'); - const set = readBeaconSet(); + const set = readBeaconSet(tmpDir); expect(set).toBeInstanceOf(Set); expect(set.size).toBe(0); }); test('readBeaconSet returns primary only when no secondary', async () => { const { writeProjectConfig, readBeaconSet } = await import('../src/lib/vit-dir.js'); - writeProjectConfig({ beacon: 'vit:github.com/org/repo' }); - const set = readBeaconSet(); + writeProjectConfig({ beacon: 'vit:github.com/org/repo' }, tmpDir); + const set = readBeaconSet(tmpDir); expect(set.size).toBe(1); expect(set.has('vit:github.com/org/repo')).toBe(true); }); test('readBeaconSet returns both when secondary is set', async () => { const { writeProjectConfig, readBeaconSet } = await import('../src/lib/vit-dir.js'); - writeProjectConfig({ beacon: 'vit:github.com/org/repo', secondaryBeacon: 'vit:github.com/upstream/repo' }); - const set = readBeaconSet(); + writeProjectConfig({ beacon: 'vit:github.com/org/repo', secondaryBeacon: 'vit:github.com/upstream/repo' }, tmpDir); + const set = readBeaconSet(tmpDir); expect(set.size).toBe(2); expect(set.has('vit:github.com/org/repo')).toBe(true); expect(set.has('vit:github.com/upstream/repo')).toBe(true); @@ -71,7 +66,7 @@ describe('vit-dir', () => { test('appendLog creates file and appends JSONL line', async () => { const { appendLog } = await import('../src/lib/vit-dir.js'); - appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', did: 'did:plc:test' }); + appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', did: 'did:plc:test' }, tmpDir); const content = readFileSync(join(tmpDir, '.vit', 'caps.jsonl'), 'utf-8'); const lines = content.trim().split('\n'); expect(lines.length).toBe(1); @@ -80,8 +75,8 @@ describe('vit-dir', () => { test('appendLog appends to existing file', async () => { const { appendLog } = await import('../src/lib/vit-dir.js'); - appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', n: 1 }); - appendLog('caps.jsonl', { ts: '2026-01-02T00:00:00Z', n: 2 }); + appendLog('caps.jsonl', { ts: '2026-01-01T00:00:00Z', n: 1 }, tmpDir); + appendLog('caps.jsonl', { ts: '2026-01-02T00:00:00Z', n: 2 }, tmpDir); const content = readFileSync(join(tmpDir, '.vit', 'caps.jsonl'), 'utf-8'); const lines = content.trim().split('\n'); expect(lines.length).toBe(2); -- 2.51.2