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, readFileSync, existsSync } from 'node:fs';
7import { tmpdir } from 'node:os';
8import { join } from 'node:path';
9import { execSync } from 'node:child_process';
10
11describe('vit init', () => {
12 let tmpDir;
13
14 beforeEach(() => {
15 tmpDir = join(tmpdir(), '.test-tmp-' + Math.random().toString(36).slice(2));
16 mkdirSync(tmpDir, { recursive: true });
17 });
18
19 afterEach(() => {
20 rmSync(tmpDir, { recursive: true, force: true });
21 });
22
23 test('writes beacon from HTTPS URL', () => {
24 const result = run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' });
25 expect(result.exitCode).toBe(0);
26 expect(result.stdout).toContain('beacon: vit:github.com/solpbc/vit');
27
28 const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8');
29 expect(JSON.parse(content).beacon).toBe('vit:github.com/solpbc/vit');
30 });
31
32 test('writes beacon from SSH URL', () => {
33 const result = run('init --beacon git@github.com:solpbc/vit.git', tmpDir, { CLAUDECODE: '1' });
34 expect(result.exitCode).toBe(0);
35 expect(result.stdout).toContain('beacon: vit:github.com/solpbc/vit');
36
37 const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8');
38 expect(JSON.parse(content).beacon).toBe('vit:github.com/solpbc/vit');
39 });
40
41 test('creates .vit directory if missing', () => {
42 expect(existsSync(join(tmpDir, '.vit'))).toBe(false);
43 run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' });
44 expect(existsSync(join(tmpDir, '.vit'))).toBe(true);
45 });
46
47 test('generates .vit/README.md on init', () => {
48 run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' });
49 const readme = readFileSync(join(tmpDir, '.vit', 'README.md'), 'utf-8');
50 expect(readme).toContain('social open source network');
51 expect(readme).toContain('v-it.org');
52 });
53
54 test('overwrites existing beacon silently', () => {
55 run('init --beacon https://github.com/old/repo.git', tmpDir, { CLAUDECODE: '1' });
56 run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' });
57
58 const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8');
59 expect(JSON.parse(content).beacon).toBe('vit:github.com/solpbc/vit');
60 });
61
62 test('reads beacon from git remote with --beacon .', () => {
63 // Set up a git repo with a remote origin
64 execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
65 execSync('git remote add origin https://github.com/solpbc/vit.git', { cwd: tmpDir, stdio: 'pipe' });
66
67 const result = run('init --beacon .', tmpDir, { CLAUDECODE: '1' });
68 expect(result.exitCode).toBe(0);
69 expect(result.stdout).toContain('beacon: vit:github.com/solpbc/vit');
70 });
71
72 test('errors when --beacon . has no git remote', () => {
73 // tmpDir is not a git repo
74 const result = run('init --beacon .', tmpDir, { CLAUDECODE: '1' });
75 expect(result.exitCode).not.toBe(0);
76 });
77
78 test('reports beacon when no flag and beacon exists', () => {
79 run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' });
80 const result = run('init', tmpDir, { CLAUDECODE: '1' });
81 expect(result.exitCode).toBe(0);
82 expect(result.stdout).toContain('beacon: vit:github.com/solpbc/vit');
83 expect(result.stdout).toContain('hint: to change the beacon, run: vit init --beacon <git-url>');
84 });
85
86 test('--secondary stores secondaryBeacon with existing primary', () => {
87 run('init --beacon https://github.com/org/repo.git', tmpDir, { CLAUDECODE: '1' });
88 const result = run('init --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' });
89 expect(result.exitCode).toBe(0);
90
91 const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8');
92 const config = JSON.parse(content);
93 expect(config.beacon).toBe('vit:github.com/org/repo');
94 expect(config.secondaryBeacon).toBe('vit:github.com/upstream/repo');
95 });
96
97 test('--secondary errors without existing primary beacon', () => {
98 const result = run('init --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' });
99 expect(result.exitCode).not.toBe(0);
100 });
101
102 test('--beacon preserves existing secondaryBeacon', () => {
103 run('init --beacon https://github.com/org/repo.git', tmpDir, { CLAUDECODE: '1' });
104 run('init --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' });
105 run('init --beacon https://github.com/org/newrepo.git', tmpDir, { CLAUDECODE: '1' });
106
107 const content = readFileSync(join(tmpDir, '.vit', 'config.json'), 'utf-8');
108 const config = JSON.parse(content);
109 expect(config.beacon).toBe('vit:github.com/org/newrepo');
110 expect(config.secondaryBeacon).toBe('vit:github.com/upstream/repo');
111 });
112
113 test('displays secondary beacon when set', () => {
114 run('init --beacon https://github.com/org/repo.git --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' });
115 const result = run('init', tmpDir, { CLAUDECODE: '1' });
116 expect(result.exitCode).toBe(0);
117 expect(result.stdout).toContain('beacon: vit:github.com/org/repo');
118 expect(result.stdout).toContain('secondary beacon: vit:github.com/upstream/repo');
119 });
120
121 test('--json includes secondaryBeacon when set', () => {
122 run('init --beacon https://github.com/org/repo.git --secondary https://github.com/upstream/repo.git', tmpDir, { CLAUDECODE: '1' });
123 const result = run('init --json', tmpDir, { CLAUDECODE: '1' });
124 expect(result.exitCode).toBe(0);
125 const output = JSON.parse(result.stdout);
126 expect(output.beacon).toBe('vit:github.com/org/repo');
127 expect(output.secondaryBeacon).toBe('vit:github.com/upstream/repo');
128 });
129
130 test('reports no beacon when .vit exists but directory is not a git repo', () => {
131 mkdirSync(join(tmpDir, '.vit'), { recursive: true });
132 const result = run('init', tmpDir, { CLAUDECODE: '1' });
133 expect(result.exitCode).toBe(0);
134 expect(result.stdout).toContain('status: no beacon');
135 expect(result.stdout).toContain('git: false');
136 expect(result.stdout).toContain('hint: run: vit init --beacon <canonical-git-url>');
137 });
138
139 test('reports .vit not found when no flag and no .vit dir', () => {
140 const result = run('init', tmpDir, { CLAUDECODE: '1' });
141 expect(result.exitCode).toBe(0);
142 expect(result.stdout).toContain('status: not initialized');
143 expect(result.stdout).toContain('git: false');
144 expect(result.stdout).toContain('hint: run vit init from inside a git repository');
145 });
146
147 test('guides agent in fork repo with upstream and origin remotes', () => {
148 execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
149 execSync('git remote add origin https://github.com/agent/vit.git', { cwd: tmpDir, stdio: 'pipe' });
150 execSync('git remote add upstream https://github.com/solpbc/vit.git', { cwd: tmpDir, stdio: 'pipe' });
151
152 const result = run('init', tmpDir, { CLAUDECODE: '1' });
153 expect(result.exitCode).toBe(0);
154 expect(result.stdout).toContain('status: not initialized');
155 expect(result.stdout).toContain('git: true');
156 expect(result.stdout).toContain('origin=');
157 expect(result.stdout).toContain('upstream=');
158 expect(result.stdout).toContain('hint: detected upstream remote');
159 expect(result.stdout).toContain('vit init --beacon https://github.com/solpbc/vit.git');
160 });
161
162 test('guides agent in repo with only origin remote', () => {
163 execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
164 execSync('git remote add origin https://github.com/solpbc/vit.git', { cwd: tmpDir, stdio: 'pipe' });
165
166 const result = run('init', tmpDir, { CLAUDECODE: '1' });
167 expect(result.exitCode).toBe(0);
168 expect(result.stdout).toContain('status: not initialized');
169 expect(result.stdout).toContain('git: true');
170 expect(result.stdout).toContain('origin=');
171 expect(result.stdout).toContain('vit init --beacon https://github.com/solpbc/vit.git');
172 });
173
174 test('guides agent in git repo with no remotes', () => {
175 execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
176
177 const result = run('init', tmpDir, { CLAUDECODE: '1' });
178 expect(result.exitCode).toBe(0);
179 expect(result.stdout).toContain('status: not initialized');
180 expect(result.stdout).toContain('git: true');
181 expect(result.stdout).toContain('remotes: none');
182 expect(result.stdout).toContain('hint: no git remotes found');
183 });
184
185 test('guides agent in git repo with .vit but no beacon', () => {
186 execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
187 mkdirSync(join(tmpDir, '.vit'), { recursive: true });
188
189 const result = run('init', tmpDir, { CLAUDECODE: '1' });
190 expect(result.exitCode).toBe(0);
191 expect(result.stdout).toContain('status: no beacon');
192 expect(result.stdout).toContain('git: true');
193 expect(result.stdout).toContain('remotes: none');
194 expect(result.stdout).toContain('hint: no git remotes found');
195 });
196
197 test('--beacon . prefers upstream over origin in fork', () => {
198 execSync('git init', { cwd: tmpDir, stdio: 'pipe' });
199 execSync('git remote add origin https://github.com/agent/fork.git', { cwd: tmpDir, stdio: 'pipe' });
200 execSync('git remote add upstream https://github.com/solpbc/vit.git', { cwd: tmpDir, stdio: 'pipe' });
201
202 const result = run('init --beacon .', tmpDir, { CLAUDECODE: '1' });
203 expect(result.exitCode).toBe(0);
204 expect(result.stdout).toContain('beacon: vit:github.com/solpbc/vit');
205 });
206
207 test('shows guidance for already initialized repo', () => {
208 run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '1' });
209 const result = run('init', tmpDir, { CLAUDECODE: '1' });
210 expect(result.exitCode).toBe(0);
211 expect(result.stdout).toContain('beacon: vit:github.com/solpbc/vit');
212 expect(result.stdout).toContain('hint: to change the beacon');
213 });
214
215 test('errors on invalid git URL', () => {
216 const result = run('init --beacon notaurl', tmpDir, { CLAUDECODE: '1' });
217 expect(result.exitCode).not.toBe(0);
218 });
219
220 test('rejects when run outside a coding agent', () => {
221 const result = run('init --beacon https://github.com/solpbc/vit.git', tmpDir, { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' });
222 expect(result.exitCode).toBe(1);
223 expect(result.stderr).toContain('should be run by a coding agent');
224 });
225});