rolecraft 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,228 @@
1
+ import { describe, it, before, after } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import {
4
+ mkdtempSync, mkdirSync, writeFileSync, symlinkSync,
5
+ } from 'node:fs'
6
+ import { rm } from 'node:fs/promises'
7
+ import { join } from 'node:path'
8
+ import { tmpdir } from 'node:os'
9
+
10
+ let tempDir, resolverModule
11
+
12
+ async function freshImport() {
13
+ resolverModule = await import('./resolver.js')
14
+ }
15
+
16
+ describe('resolver', () => {
17
+ before(() => {
18
+ tempDir = mkdtempSync(join(tmpdir(), 'rolecraft-resolver-test-'))
19
+ })
20
+
21
+ after(async () => {
22
+ await rm(tempDir, { recursive: true, force: true })
23
+ })
24
+
25
+ describe('resolveSource', () => {
26
+ it('throws for invalid source', async () => {
27
+ await freshImport()
28
+ await assert.rejects(
29
+ () => resolverModule.resolveSource('invalid-format'),
30
+ /Invalid source/,
31
+ )
32
+ })
33
+
34
+ it('recognises local dot-path', async () => {
35
+ await freshImport()
36
+ const relDir = 'dot-local-skill'
37
+ const absDir = join(process.cwd(), relDir)
38
+ mkdirSync(absDir, { recursive: true })
39
+ writeFileSync(join(absDir, 'SKILL.md'), '# slug: test/dot\nname: dot-path\nContent')
40
+
41
+ const result = await resolverModule.resolveSource('./' + relDir)
42
+ assert.equal(result.name, 'dot-path')
43
+ assert.equal(result.sourceType, 'local')
44
+ await rm(absDir, { recursive: true, force: true })
45
+ })
46
+
47
+ it('recognises absolute path', async () => {
48
+ await freshImport()
49
+ const skillDir = join(tempDir, 'abs-skill')
50
+ mkdirSync(skillDir, { recursive: true })
51
+ writeFileSync(join(skillDir, 'SKILL.md'), '# slug: test/abs\nname: abs-skill\nContent')
52
+
53
+ const result = await resolverModule.resolveSource(skillDir)
54
+ assert.equal(result.name, 'abs-skill')
55
+ })
56
+ })
57
+
58
+ describe('resolveLocal', () => {
59
+ it('resolves a SKILL.md file path', async () => {
60
+ const skillDir = join(tempDir, 'file-skill')
61
+ mkdirSync(skillDir, { recursive: true })
62
+ writeFileSync(join(skillDir, 'SKILL.md'), '# slug: a/b\nname: file-skill\n# owner: someone\nData')
63
+
64
+ const result = await resolverModule.resolveSource(join(skillDir, 'SKILL.md'))
65
+ assert.equal(result.name, 'file-skill')
66
+ assert.equal(result.slug, 'a/b')
67
+ assert.equal(result.owner, 'someone')
68
+ assert.equal(result.sourceType, 'local')
69
+ })
70
+
71
+ it('resolves a directory containing SKILL.md', async () => {
72
+ const skillDir = join(tempDir, 'dir-skill')
73
+ mkdirSync(skillDir, { recursive: true })
74
+ writeFileSync(join(skillDir, 'SKILL.md'), '# slug: c/d\nname: dir-skill\nContent')
75
+
76
+ const result = await resolverModule.resolveSource(skillDir)
77
+ assert.equal(result.name, 'dir-skill')
78
+ })
79
+
80
+ it('throws for non-SKILL.md file', async () => {
81
+ const p = join(tempDir, 'readme.txt')
82
+ writeFileSync(p, 'hello')
83
+ await assert.rejects(
84
+ () => resolverModule.resolveSource(p),
85
+ /Source must be a SKILL.md file or a directory containing one/,
86
+ )
87
+ })
88
+
89
+ it('throws when no SKILL.md found in directory', async () => {
90
+ const d = join(tempDir, 'empty-dir')
91
+ mkdirSync(d, { recursive: true })
92
+ await assert.rejects(
93
+ () => resolverModule.resolveSource(d),
94
+ /No SKILL.md found/,
95
+ )
96
+ })
97
+
98
+ it('handles ~ expansion', async () => {
99
+ const origHome = process.env.HOME
100
+ process.env.HOME = tempDir
101
+ await freshImport()
102
+
103
+ const skillDir = join(tempDir, 'tilde-skill')
104
+ mkdirSync(skillDir, { recursive: true })
105
+ writeFileSync(join(skillDir, 'SKILL.md'), '# slug: t/tilde\nname: tilde-skill\nContent')
106
+
107
+ const result = await resolverModule.resolveSource('~/tilde-skill')
108
+ assert.equal(result.name, 'tilde-skill')
109
+ process.env.HOME = origHome
110
+ })
111
+
112
+ it('scans subdirectories recursively', async () => {
113
+ const parent = join(tempDir, 'parent')
114
+ const nested = join(parent, 'sub', 'deep')
115
+ mkdirSync(nested, { recursive: true })
116
+ writeFileSync(join(nested, 'SKILL.md'), '# slug: x/nested\nname: nested-skill\nContent')
117
+
118
+ const result = await resolverModule.resolveSource(parent)
119
+ assert.equal(result.name, 'nested-skill')
120
+ })
121
+
122
+ it('skips .git directories during scan', async () => {
123
+ const parent = join(tempDir, 'with-git')
124
+ mkdirSync(join(parent, '.git'), { recursive: true })
125
+ mkdirSync(join(parent, 'real'), { recursive: true })
126
+ writeFileSync(join(parent, 'real', 'SKILL.md'), '# slug: r/real\nname: real-skill\nContent')
127
+
128
+ const result = await resolverModule.resolveSource(parent)
129
+ assert.equal(result.name, 'real-skill')
130
+ })
131
+
132
+ it('respects maxDepth in scan', async () => {
133
+ const parent = join(tempDir, 'deep-parent')
134
+ const tooDeep = join(parent, 'a', 'b', 'c', 'd')
135
+ mkdirSync(tooDeep, { recursive: true })
136
+ writeFileSync(join(tooDeep, 'SKILL.md'), '# slug: d/deep\nname: deep-skill\nContent')
137
+
138
+ await assert.rejects(
139
+ () => resolverModule.resolveSource(parent),
140
+ /No SKILL.md found/,
141
+ )
142
+ })
143
+
144
+ it('handles scan read errors gracefully', async () => {
145
+ const parent = join(tempDir, 'bad-scan')
146
+ mkdirSync(parent, { recursive: true })
147
+ symlinkSync('/nonexistent-target', join(parent, 'SKILL.md'))
148
+
149
+ await assert.rejects(
150
+ () => resolverModule.resolveSource(parent),
151
+ /No SKILL.md found/,
152
+ )
153
+ })
154
+
155
+ it('handles unreadable directory during scan', async () => {
156
+ const { chmodSync } = await import('node:fs')
157
+ const parent = join(tempDir, 'partial-scan')
158
+ mkdirSync(join(parent, 'good'), { recursive: true })
159
+ mkdirSync(join(parent, 'locked'), { recursive: true })
160
+ chmodSync(join(parent, 'locked'), 0o000)
161
+ writeFileSync(join(parent, 'good', 'SKILL.md'), '# slug: s/good\nname: good\nContent')
162
+
163
+ const result = await resolverModule.resolveSource(parent)
164
+ assert.equal(result.name, 'good')
165
+
166
+ chmodSync(join(parent, 'locked'), 0o755)
167
+ })
168
+
169
+ it('includes files list in result, excluding .git', async () => {
170
+ const skillDir = join(tempDir, 'multi-file')
171
+ mkdirSync(skillDir, { recursive: true })
172
+ writeFileSync(join(skillDir, 'SKILL.md'), '# slug: m/multi\nname: multi\nContent')
173
+ writeFileSync(join(skillDir, 'helper.js'), 'x')
174
+ writeFileSync(join(skillDir, 'config.json'), '{}')
175
+
176
+ const result = await resolverModule.resolveSource(skillDir)
177
+ assert.ok(result.files.includes('SKILL.md'))
178
+ assert.ok(result.files.includes('helper.js'))
179
+ assert.ok(result.files.includes('config.json'))
180
+ assert.ok(!result.files.includes('.git'))
181
+ })
182
+ })
183
+
184
+ describe('parseMetadata edge cases', () => {
185
+ it('uses name from name field over slug-derived name', async () => {
186
+ const d = join(tempDir, 'meta1')
187
+ mkdirSync(d, { recursive: true })
188
+ writeFileSync(join(d, 'SKILL.md'), '# slug: owner/name\nname: my-name\n# owner: me\nContent')
189
+ const r = await resolverModule.resolveSource(d)
190
+ assert.equal(r.name, 'my-name')
191
+ assert.equal(r.slug, 'owner/name')
192
+ assert.equal(r.owner, 'me')
193
+ })
194
+
195
+ it('derives name from slug when no explicit name', async () => {
196
+ const d = join(tempDir, 'meta2')
197
+ mkdirSync(d, { recursive: true })
198
+ writeFileSync(join(d, 'SKILL.md'), '# slug: owner/name\nContent')
199
+ const r = await resolverModule.resolveSource(d)
200
+ assert.equal(r.name, 'name')
201
+ assert.equal(r.slug, 'owner/name')
202
+ assert.equal(r.owner, 'local')
203
+ })
204
+
205
+ it('defaults to unknown when no slug or name', async () => {
206
+ const d = join(tempDir, 'meta3')
207
+ mkdirSync(d, { recursive: true })
208
+ writeFileSync(join(d, 'SKILL.md'), 'Some content without metadata')
209
+ const r = await resolverModule.resolveSource(d)
210
+ assert.equal(r.name, 'unknown')
211
+ assert.equal(r.slug, 'unknown')
212
+ assert.equal(r.owner, 'local')
213
+ })
214
+ })
215
+
216
+ describe('resolveGitHub', () => {
217
+ it('throws for invalid GitHub ref', async () => {
218
+ // Invalid GitHub ref falls through to isGitHubRef check
219
+ // A valid-looking ref (owner/repo) triggers GitHub resolution
220
+ // which requires cloning, so we test the error path
221
+ await freshImport()
222
+ await assert.rejects(
223
+ () => resolverModule.resolveSource('a'),
224
+ /Invalid source/,
225
+ )
226
+ })
227
+ })
228
+ })