happyskills 0.35.5 → 0.37.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,231 @@
1
+ 'use strict'
2
+ const { describe, it, beforeEach, afterEach } = require('node:test')
3
+ const assert = require('node:assert/strict')
4
+ const fs = require('fs')
5
+ const os = require('os')
6
+ const path = require('path')
7
+
8
+ const { validate_dependencies } = require('./dependency_rules')
9
+
10
+ const make_tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'hs-validate-deps-'))
11
+
12
+ let tmp
13
+ beforeEach(() => { tmp = make_tmp() })
14
+ afterEach(() => { fs.rmSync(tmp, { recursive: true, force: true }) })
15
+
16
+ // ---------------------------------------------------------------------------
17
+ // Local rules (no registry)
18
+ // ---------------------------------------------------------------------------
19
+
20
+ describe('dep-self-reference', () => {
21
+ it('passes when no dependencies', async () => {
22
+ const manifest = { name: 'my-skill', version: '1.0.0' }
23
+ const [err, results] = await validate_dependencies(tmp, manifest)
24
+ assert.ifError(err)
25
+ const check = results.find(r => r.rule === 'dep-self-reference')
26
+ assert.strictEqual(check.severity, 'pass')
27
+ })
28
+
29
+ it('passes when dependencies do not include self', async () => {
30
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/other': '*' } }
31
+ const [err, results] = await validate_dependencies(tmp, manifest)
32
+ assert.ifError(err)
33
+ const check = results.find(r => r.rule === 'dep-self-reference')
34
+ assert.strictEqual(check.severity, 'pass')
35
+ })
36
+
37
+ it('errors when skill depends on itself (short name match)', async () => {
38
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/my-skill': '*' } }
39
+ const [err, results] = await validate_dependencies(tmp, manifest)
40
+ assert.ifError(err)
41
+ const check = results.find(r => r.rule === 'dep-self-reference' && r.severity === 'error')
42
+ assert.ok(check, 'should find self-reference error')
43
+ assert.ok(check.message.includes('depends on itself'))
44
+ })
45
+ })
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Registry rules (mocked API)
49
+ // ---------------------------------------------------------------------------
50
+
51
+ const make_mock_api = (repos = {}, resolve_result = null) => ({
52
+ get_repo: async (owner, name, opts) => {
53
+ const key = `${owner}/${name}`
54
+ if (repos[key]) return [null, repos[key]]
55
+ return [new Error(`Not found: ${key}`), null]
56
+ },
57
+ resolve_dependencies: async (skill, version, installed) => {
58
+ if (resolve_result) return [null, resolve_result]
59
+ return [new Error('Not implemented'), null]
60
+ }
61
+ })
62
+
63
+ describe('dep-exists (registry)', () => {
64
+ it('passes when all deps exist', async () => {
65
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/auth': '*' } }
66
+ const api = make_mock_api({ 'acme/auth': { visibility: 'public' } })
67
+ const [err, results] = await validate_dependencies(tmp, manifest, { registry: true, repos_api: api })
68
+ assert.ifError(err)
69
+ const check = results.find(r => r.rule === 'dep-exists' && r.value === 'acme/auth')
70
+ assert.strictEqual(check.severity, 'pass')
71
+ })
72
+
73
+ it('errors when dep is missing', async () => {
74
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/missing': '*' } }
75
+ const api = make_mock_api({})
76
+ const [err, results] = await validate_dependencies(tmp, manifest, { registry: true, repos_api: api })
77
+ assert.ifError(err)
78
+ const check = results.find(r => r.rule === 'dep-exists' && r.value === 'acme/missing')
79
+ assert.strictEqual(check.severity, 'error')
80
+ assert.ok(check.message.includes('not found'))
81
+ })
82
+ })
83
+
84
+ describe('dep-visibility-direct (registry)', () => {
85
+ it('errors when public skill depends on private dep', async () => {
86
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/secret': '*' } }
87
+ const api = make_mock_api({ 'acme/secret': { visibility: 'private' } })
88
+ const [err, results] = await validate_dependencies(tmp, manifest, { registry: true, visibility: 'public', repos_api: api })
89
+ assert.ifError(err)
90
+ const check = results.find(r => r.rule === 'dep-visibility-direct' && r.severity === 'error')
91
+ assert.ok(check, 'should find visibility error')
92
+ assert.ok(check.message.includes('Visibility mismatch'))
93
+ assert.ok(check.message.includes('private'))
94
+ })
95
+
96
+ it('warns when public skill depends on workspace-scoped dep', async () => {
97
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/internal': '*' } }
98
+ const api = make_mock_api({ 'acme/internal': { visibility: 'workspace' } })
99
+ const [err, results] = await validate_dependencies(tmp, manifest, { registry: true, visibility: 'public', repos_api: api })
100
+ assert.ifError(err)
101
+ const check = results.find(r => r.rule === 'dep-visibility-workspace')
102
+ assert.strictEqual(check.severity, 'warning')
103
+ })
104
+
105
+ it('passes when public skill has all public deps', async () => {
106
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/auth': '*' } }
107
+ const api = make_mock_api({ 'acme/auth': { visibility: 'public' } })
108
+ const [err, results] = await validate_dependencies(tmp, manifest, { registry: true, visibility: 'public', repos_api: api })
109
+ assert.ifError(err)
110
+ const check = results.find(r => r.rule === 'dep-visibility-direct')
111
+ assert.strictEqual(check.severity, 'pass')
112
+ })
113
+
114
+ it('skips visibility check for private skills', async () => {
115
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/secret': '*' } }
116
+ const api = make_mock_api({ 'acme/secret': { visibility: 'private' } })
117
+ const [err, results] = await validate_dependencies(tmp, manifest, { registry: true, visibility: 'private', repos_api: api })
118
+ assert.ifError(err)
119
+ const vis_results = results.filter(r => r.rule === 'dep-visibility-direct' || r.rule === 'dep-visibility-workspace')
120
+ assert.strictEqual(vis_results.length, 0)
121
+ })
122
+ })
123
+
124
+ describe('dep-circular (registry)', () => {
125
+ it('detects a simple cycle A → B → A', async () => {
126
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/b': '*' } }
127
+ const resolve_result = {
128
+ packages: [
129
+ { skill: 'acme/a', version: '1.0.0', dependencies: { 'acme/b': '*' } },
130
+ { skill: 'acme/b', version: '1.0.0', dependencies: { 'acme/a': '*' } }
131
+ ],
132
+ skipped: []
133
+ }
134
+ const api = make_mock_api({ 'acme/b': { visibility: 'public' } }, resolve_result)
135
+ const [err, results] = await validate_dependencies(tmp, manifest, {
136
+ registry: true, full_name: 'acme/a', repos_api: api
137
+ })
138
+ assert.ifError(err)
139
+ const check = results.find(r => r.rule === 'dep-circular' && r.severity === 'error')
140
+ assert.ok(check, 'should detect cycle')
141
+ assert.ok(check.message.includes('Circular dependency'))
142
+ })
143
+
144
+ it('passes when no cycles', async () => {
145
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/b': '*' } }
146
+ const resolve_result = {
147
+ packages: [
148
+ { skill: 'acme/a', version: '1.0.0', dependencies: { 'acme/b': '*' } },
149
+ { skill: 'acme/b', version: '1.0.0', dependencies: {} }
150
+ ],
151
+ skipped: []
152
+ }
153
+ const api = make_mock_api({ 'acme/b': { visibility: 'public' } }, resolve_result)
154
+ const [err, results] = await validate_dependencies(tmp, manifest, {
155
+ registry: true, full_name: 'acme/a', repos_api: api
156
+ })
157
+ assert.ifError(err)
158
+ const check = results.find(r => r.rule === 'dep-circular')
159
+ assert.strictEqual(check.severity, 'pass')
160
+ })
161
+ })
162
+
163
+ describe('dep-visibility-transitive (registry)', () => {
164
+ it('errors when transitive dep is private', async () => {
165
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/b': '*' } }
166
+ const resolve_result = {
167
+ packages: [
168
+ { skill: 'acme/a', version: '1.0.0', visibility: 'public', dependencies: { 'acme/b': '*' } },
169
+ { skill: 'acme/b', version: '1.0.0', visibility: 'public', dependencies: { 'acme/c': '*' } },
170
+ { skill: 'acme/c', version: '1.0.0', visibility: 'private', dependencies: {} }
171
+ ],
172
+ skipped: []
173
+ }
174
+ const api = make_mock_api(
175
+ { 'acme/b': { visibility: 'public' } },
176
+ resolve_result
177
+ )
178
+ const [err, results] = await validate_dependencies(tmp, manifest, {
179
+ registry: true, visibility: 'public', full_name: 'acme/a', repos_api: api
180
+ })
181
+ assert.ifError(err)
182
+ const check = results.find(r => r.rule === 'dep-visibility-transitive' && r.severity === 'error')
183
+ assert.ok(check, 'should find transitive visibility error')
184
+ assert.ok(check.message.includes('acme/c'))
185
+ assert.ok(check.message.includes('private'))
186
+ })
187
+
188
+ it('reports skipped (access-denied) deps as visibility errors', async () => {
189
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/b': '*' } }
190
+ const resolve_result = {
191
+ packages: [
192
+ { skill: 'acme/a', version: '1.0.0', visibility: 'public', dependencies: { 'acme/b': '*' } },
193
+ { skill: 'acme/b', version: '1.0.0', visibility: 'public', dependencies: { 'acme/secret': '*' } }
194
+ ],
195
+ skipped: [
196
+ { skill: 'acme/secret', reason: 'access_denied', message: 'Access denied', required_by: 'acme/b', visibility: 'private' }
197
+ ]
198
+ }
199
+ const api = make_mock_api(
200
+ { 'acme/b': { visibility: 'public' } },
201
+ resolve_result
202
+ )
203
+ const [err, results] = await validate_dependencies(tmp, manifest, {
204
+ registry: true, visibility: 'public', full_name: 'acme/a', repos_api: api
205
+ })
206
+ assert.ifError(err)
207
+ const check = results.find(r => r.rule === 'dep-visibility-transitive' && r.severity === 'error' && r.value === 'acme/secret')
208
+ assert.ok(check, 'should report skipped dep as visibility error')
209
+ })
210
+
211
+ it('passes when all transitive deps are public', async () => {
212
+ const manifest = { name: 'my-skill', version: '1.0.0', dependencies: { 'acme/b': '*' } }
213
+ const resolve_result = {
214
+ packages: [
215
+ { skill: 'acme/a', version: '1.0.0', visibility: 'public', dependencies: { 'acme/b': '*' } },
216
+ { skill: 'acme/b', version: '1.0.0', visibility: 'public', dependencies: {} }
217
+ ],
218
+ skipped: []
219
+ }
220
+ const api = make_mock_api(
221
+ { 'acme/b': { visibility: 'public' } },
222
+ resolve_result
223
+ )
224
+ const [err, results] = await validate_dependencies(tmp, manifest, {
225
+ registry: true, visibility: 'public', full_name: 'acme/a', repos_api: api
226
+ })
227
+ assert.ifError(err)
228
+ const check = results.find(r => r.rule === 'dep-visibility-transitive')
229
+ assert.strictEqual(check.severity, 'pass')
230
+ })
231
+ })