open source is social v-it.org
1// SPDX-License-Identifier: MIT
2// Copyright (c) 2026 sol pbc
3
4import { describe, test, expect } from 'bun:test';
5import { run } from './helpers.js';
6import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
7import { tmpdir } from 'node:os';
8import { join } from 'node:path';
9
10const agentEnv = { CLAUDECODE: '1' };
11
12describe('vit learn', () => {
13 test('rejects when run outside a coding agent', () => {
14 const r = run('learn skill-test', '/tmp', { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' });
15 expect(r.exitCode).not.toBe(0);
16 expect(r.stderr).toContain('should be run by a coding agent');
17 });
18
19 test('rejects non-skill ref format', () => {
20 const r = run('learn fast-cache-invalidation', '/tmp', agentEnv);
21 expect(r.exitCode).not.toBe(0);
22 expect(r.stderr).toContain('invalid skill ref');
23 });
24
25 test('rejects invalid skill name in ref', () => {
26 const r = run('learn skill-Bad-Name', '/tmp', agentEnv);
27 expect(r.exitCode).not.toBe(0);
28 expect(r.stderr).toContain('invalid skill ref');
29 });
30
31 test('rejects skill ref with consecutive hyphens', () => {
32 const r = run('learn skill-bad--name', '/tmp', agentEnv);
33 expect(r.exitCode).not.toBe(0);
34 expect(r.stderr).toContain('invalid skill ref');
35 });
36
37 test('fails when no arguments provided', () => {
38 const r = run('learn', '/tmp', agentEnv);
39 expect(r.exitCode).not.toBe(0);
40 });
41
42 test('requires vet for --user install', () => {
43 const tmp = join(tmpdir(), '.test-learn-user-' + Math.random().toString(36).slice(2));
44 mkdirSync(join(tmp, '.vit'), { recursive: true });
45 const r = run('learn skill-test --user --did did:plc:test123', tmp, agentEnv);
46 expect(r.exitCode).not.toBe(0);
47 expect(r.stderr).toContain('not yet vetted');
48 expect(r.stderr).toContain('user-wide install requires vetting');
49 rmSync(tmp, { recursive: true, force: true });
50 });
51
52 test('requires vet for project-level install without dangerous-accept', () => {
53 const tmp = join(tmpdir(), '.test-learn-proj-' + Math.random().toString(36).slice(2));
54 mkdirSync(join(tmp, '.vit'), { recursive: true });
55 const r = run('learn skill-test --did did:plc:test123', tmp, agentEnv);
56 expect(r.exitCode).not.toBe(0);
57 expect(r.stderr).toContain('not yet vetted');
58 rmSync(tmp, { recursive: true, force: true });
59 });
60
61 // --- trust gate tests ---
62
63 describe('trust gate', () => {
64 test('CLAUDE_SKIP_PERMISSIONS env var no longer bypasses vet', () => {
65 const tmp = join(tmpdir(), '.test-learn-noskip-' + Math.random().toString(36).slice(2));
66 mkdirSync(join(tmp, '.vit'), { recursive: true });
67 const r = run('learn skill-test --did did:plc:test123', tmp, { ...agentEnv, CLAUDE_SKIP_PERMISSIONS: '1' });
68 expect(r.exitCode).not.toBe(0);
69 // Should STILL fail at vet check — env var no longer works
70 expect(r.stderr).toContain('not yet vetted');
71 rmSync(tmp, { recursive: true, force: true });
72 });
73
74 test('dangerous-accept bypasses vet for project-level install', () => {
75 const tmp = join(tmpdir(), '.test-learn-da-' + Math.random().toString(36).slice(2));
76 mkdirSync(join(tmp, '.vit'), { recursive: true });
77 writeFileSync(join(tmp, '.vit', 'dangerous-accept'), JSON.stringify({ acceptedAt: '2026-03-26T14:30:00.000Z' }));
78 const r = run('learn skill-test --did did:plc:test123', tmp, agentEnv);
79 // Should bypass vet check — will fail later at auth, NOT at vet
80 expect(r.stderr).not.toContain('not yet vetted');
81 rmSync(tmp, { recursive: true, force: true });
82 });
83
84 test('dangerous-accept does NOT bypass vet for --user install', () => {
85 const tmp = join(tmpdir(), '.test-learn-da-user-' + Math.random().toString(36).slice(2));
86 mkdirSync(join(tmp, '.vit'), { recursive: true });
87 writeFileSync(join(tmp, '.vit', 'dangerous-accept'), JSON.stringify({ acceptedAt: '2026-03-26T14:30:00.000Z' }));
88 const r = run('learn skill-test --user --did did:plc:test123', tmp, agentEnv);
89 expect(r.exitCode).not.toBe(0);
90 // Should STILL fail at vet check for --user
91 expect(r.stderr).toContain('not yet vetted');
92 expect(r.stderr).toContain('user-wide install requires vetting');
93 rmSync(tmp, { recursive: true, force: true });
94 });
95
96 test('error includes dangerous-accept hint when agent detected', () => {
97 const tmp = join(tmpdir(), '.test-learn-hint-' + Math.random().toString(36).slice(2));
98 mkdirSync(join(tmp, '.vit'), { recursive: true });
99 const r = run('learn skill-test --did did:plc:test123', tmp, agentEnv);
100 expect(r.exitCode).not.toBe(0);
101 expect(r.stderr).toContain('vit vet --dangerous-accept --confirm');
102 rmSync(tmp, { recursive: true, force: true });
103 });
104
105 test('vet check happens before network call', () => {
106 const tmp = join(tmpdir(), '.test-learn-trust-' + Math.random().toString(36).slice(2));
107 mkdirSync(join(tmp, '.vit'), { recursive: true });
108 const r = run('learn skill-test --did did:plc:test123', tmp, agentEnv);
109 expect(r.exitCode).not.toBe(0);
110 expect(r.stderr).toContain('not yet vetted');
111 expect(r.stderr).toContain('vit vet skill-test');
112 rmSync(tmp, { recursive: true, force: true });
113 });
114 });
115
116 describe('vit learn @handle/', () => {
117 test('parses @handle/name format', () => {
118 const r = run('learn @test.example/my-skill', '/tmp');
119 expect(r.exitCode).not.toBe(0);
120 expect(r.stderr).not.toContain('invalid skill ref');
121 });
122
123 test('rejects @handle/ with no skill name', () => {
124 const r = run('learn @test.example/', '/tmp');
125 expect(r.exitCode).not.toBe(0);
126 expect(r.stderr).toContain('invalid ref');
127 });
128
129 test('rejects @/name with no handle', () => {
130 const r = run('learn @/my-skill', '/tmp');
131 expect(r.exitCode).not.toBe(0);
132 expect(r.stderr).toContain('invalid ref');
133 });
134
135 test('rejects handle without dot', () => {
136 const r = run('learn @localhost/my-skill', '/tmp');
137 expect(r.exitCode).not.toBe(0);
138 expect(r.stderr).toContain('invalid handle');
139 });
140
141 test('rejects invalid skill name', () => {
142 const r = run('learn @test.example/Bad-Name', '/tmp');
143 expect(r.exitCode).not.toBe(0);
144 expect(r.stderr).toContain('invalid skill name');
145 });
146
147 test('trailing dot sets project-local', () => {
148 const r = run('learn @test.example/my-skill.', '/tmp');
149 expect(r.exitCode).not.toBe(0);
150 expect(r.stderr).not.toContain('invalid skill name');
151 });
152
153 test('@handle/ path does NOT require agent env', () => {
154 const r = run('learn @test.example/my-skill', '/tmp', { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' });
155 expect(r.exitCode).not.toBe(0);
156 expect(r.stderr).not.toContain('should be run by a coding agent');
157 });
158
159 test('@handle/ path does NOT require .vit/ dir', () => {
160 const r = run('learn @test.example/my-skill', '/tmp');
161 expect(r.exitCode).not.toBe(0);
162 expect(r.stderr).not.toContain('not yet vetted');
163 });
164
165 test('--project flag accepted', () => {
166 const r = run('learn @test.example/my-skill --project', '/tmp');
167 expect(r.exitCode).not.toBe(0);
168 expect(r.stderr).not.toContain('unknown option');
169 });
170
171 test('--dry-run flag accepted', () => {
172 const r = run('learn @test.example/my-skill --dry-run', '/tmp');
173 expect(r.exitCode).not.toBe(0);
174 expect(r.stderr).not.toContain('unknown option');
175 });
176 });
177});