This repository has no description
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 ship --skill', () => {
13 test('rejects when run outside a coding agent', () => {
14 const tmp = join(tmpdir(), '.test-ship-skill-gate-' + Math.random().toString(36).slice(2));
15 mkdirSync(tmp, { recursive: true });
16 writeFileSync(join(tmp, 'SKILL.md'), '---\nname: test\ndescription: test skill\n---\n# Test');
17 const r = run(`ship --skill ${tmp}`, '/tmp', { CLAUDECODE: '', GEMINI_CLI: '', CODEX_CI: '' });
18 expect(r.exitCode).not.toBe(0);
19 expect(r.stderr).toContain('should be run by a coding agent');
20 rmSync(tmp, { recursive: true, force: true });
21 });
22
23 test('errors when SKILL.md is missing', () => {
24 const tmp = join(tmpdir(), '.test-ship-skill-no-md-' + Math.random().toString(36).slice(2));
25 mkdirSync(tmp, { recursive: true });
26 const r = run(`ship --skill ${tmp}`, '/tmp', agentEnv);
27 expect(r.exitCode).not.toBe(0);
28 expect(r.stderr).toContain('no SKILL.md found');
29 rmSync(tmp, { recursive: true, force: true });
30 });
31
32 test('errors when SKILL.md has no name', () => {
33 const tmp = join(tmpdir(), '.test-ship-skill-no-name-' + Math.random().toString(36).slice(2));
34 mkdirSync(tmp, { recursive: true });
35 writeFileSync(join(tmp, 'SKILL.md'), '---\ndescription: test\n---\n# Test');
36 const r = run(`ship --skill ${tmp} --did did:plc:abc`, '/tmp', agentEnv);
37 expect(r.exitCode).not.toBe(0);
38 expect(r.stderr).toContain('name');
39 rmSync(tmp, { recursive: true, force: true });
40 });
41
42 test('errors when SKILL.md has no description', () => {
43 const tmp = join(tmpdir(), '.test-ship-skill-no-desc-' + Math.random().toString(36).slice(2));
44 mkdirSync(tmp, { recursive: true });
45 writeFileSync(join(tmp, 'SKILL.md'), '---\nname: test\n---\n# Test');
46 const r = run(`ship --skill ${tmp} --did did:plc:abc`, '/tmp', agentEnv);
47 expect(r.exitCode).not.toBe(0);
48 expect(r.stderr).toContain('description');
49 rmSync(tmp, { recursive: true, force: true });
50 });
51
52 test('rejects invalid skill name (uppercase)', () => {
53 const tmp = join(tmpdir(), '.test-ship-skill-bad-name-' + Math.random().toString(36).slice(2));
54 mkdirSync(tmp, { recursive: true });
55 writeFileSync(join(tmp, 'SKILL.md'), '---\nname: BadName\ndescription: test\n---\n# Test');
56 const r = run(`ship --skill ${tmp} --did did:plc:abc`, '/tmp', agentEnv);
57 expect(r.exitCode).not.toBe(0);
58 expect(r.stderr).toContain('lowercase');
59 rmSync(tmp, { recursive: true, force: true });
60 });
61
62 test('rejects skill name starting with hyphen', () => {
63 const tmp = join(tmpdir(), '.test-ship-skill-hyphen-' + Math.random().toString(36).slice(2));
64 mkdirSync(tmp, { recursive: true });
65 writeFileSync(join(tmp, 'SKILL.md'), '---\nname: -bad\ndescription: test\n---\n# Test');
66 const r = run(`ship --skill ${tmp} --did did:plc:abc`, '/tmp', agentEnv);
67 expect(r.exitCode).not.toBe(0);
68 expect(r.stderr).toContain('lowercase');
69 rmSync(tmp, { recursive: true, force: true });
70 });
71
72 test('passes validation with valid skill (fails at auth, not validation)', () => {
73 const tmp = join(tmpdir(), '.test-ship-skill-valid-' + Math.random().toString(36).slice(2));
74 mkdirSync(tmp, { recursive: true });
75 writeFileSync(join(tmp, 'SKILL.md'), '---\nname: test-skill\ndescription: a test skill\n---\n# Test');
76 const r = run(`ship --skill ${tmp} --did did:plc:abc`, '/tmp', agentEnv);
77 expect(r.exitCode).not.toBe(0);
78 // Should fail at auth, not at validation
79 expect(r.stderr).not.toContain('no SKILL.md');
80 expect(r.stderr).not.toContain('name');
81 expect(r.stderr).not.toContain('description');
82 rmSync(tmp, { recursive: true, force: true });
83 });
84
85 test('can parse the existing vit skill SKILL.md', () => {
86 // Use the real skill directory in the vit repo
87 const skillDir = join(import.meta.dir, '..', 'skills', 'vit');
88 const r = run(`ship --skill ${skillDir} --did did:plc:abc`, '/tmp', agentEnv);
89 expect(r.exitCode).not.toBe(0);
90 // Should fail at auth, not at SKILL.md parsing
91 expect(r.stderr).not.toContain('no SKILL.md');
92 expect(r.stderr).not.toContain('frontmatter must include');
93 expect(r.stderr).not.toContain('skill name must be');
94 });
95
96 test('does not require beacon for skill shipping', () => {
97 const tmp = join(tmpdir(), '.test-ship-skill-no-beacon-' + Math.random().toString(36).slice(2));
98 mkdirSync(tmp, { recursive: true });
99 writeFileSync(join(tmp, 'SKILL.md'), '---\nname: test-skill\ndescription: a test skill\n---\n# Test');
100 const r = run(`ship --skill ${tmp} --did did:plc:abc`, tmp, agentEnv);
101 expect(r.exitCode).not.toBe(0);
102 // Should NOT fail due to beacon
103 expect(r.stderr).not.toContain('no beacon set');
104 rmSync(tmp, { recursive: true, force: true });
105 });
106});