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