Skip to content

Commit 2948aef

Browse files
committed
test(registry): rewrite client tests for two-call resolve+detail flow
The old tests asserted a single GET /skills/<pkg>; the live API uses POST /skills/resolve then GET /skills/<owner>/<repo>/<name>. Also restores trailing-slash stripping on the base URL.
1 parent 0759f73 commit 2948aef

2 files changed

Lines changed: 92 additions & 14 deletions

File tree

src/registry/client.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import { ofetch } from 'ofetch'
1616
const DEFAULT_REGISTRY_URL = 'https://skilld.dev/api'
1717

1818
export function getRegistryBase(): string {
19-
return process.env.SKILLD_REGISTRY_URL || DEFAULT_REGISTRY_URL
19+
return (process.env.SKILLD_REGISTRY_URL || DEFAULT_REGISTRY_URL).replace(/\/$/, '')
2020
}
2121

2222
export interface RegistrySkill {

test/unit/registry-client.test.ts

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,31 @@ vi.mock('ofetch', () => ({
44
ofetch: vi.fn(),
55
}))
66

7+
function mockResolveAndDetail(ofetch: ReturnType<typeof vi.fn>, opts: {
8+
packageName: string
9+
owner?: string
10+
repo?: string
11+
raw?: string
12+
pushedAt?: string
13+
}) {
14+
const owner = opts.owner ?? 'antfu'
15+
const repo = opts.repo ?? 'skills'
16+
ofetch.mockResolvedValueOnce({
17+
[opts.packageName]: { owner, repo, official: false },
18+
})
19+
ofetch.mockResolvedValueOnce({
20+
owner,
21+
repo,
22+
name: opts.packageName,
23+
displayName: opts.packageName,
24+
installs: 1,
25+
branch: 'main',
26+
skillPath: `skills/${opts.packageName}/SKILL.md`,
27+
raw: opts.raw ?? '# skill',
28+
pushedAt: opts.pushedAt ?? '2026-01-01T00:00:00Z',
29+
})
30+
}
31+
732
describe('registry client', () => {
833
let originalUrl: string | undefined
934

@@ -22,52 +47,105 @@ describe('registry client', () => {
2247
it('fetchRegistrySkill uses default base when env unset', async () => {
2348
delete process.env.SKILLD_REGISTRY_URL
2449
const { ofetch } = await import('ofetch')
25-
vi.mocked(ofetch).mockResolvedValueOnce({ name: 'vue-skilld', packageName: 'vue', version: '3.5.0', content: '# vue' })
50+
mockResolveAndDetail(vi.mocked(ofetch), { packageName: 'vue' })
2651

2752
const { fetchRegistrySkill } = await import('../../src/registry/client')
28-
await fetchRegistrySkill('vue')
29-
30-
expect(ofetch).toHaveBeenCalledWith('https://skilld.dev/api/skills/vue')
53+
const skill = await fetchRegistrySkill('vue')
54+
55+
expect(skill).not.toBeNull()
56+
expect(ofetch).toHaveBeenNthCalledWith(1, 'https://skilld.dev/api/skills/resolve', {
57+
method: 'POST',
58+
body: { items: [{ packageName: 'vue', owner: undefined }] },
59+
})
60+
expect(ofetch).toHaveBeenNthCalledWith(2, 'https://skilld.dev/api/skills/antfu/skills/vue')
3161
})
3262

3363
it('fetchRegistrySkill respects SKILLD_REGISTRY_URL override', async () => {
3464
process.env.SKILLD_REGISTRY_URL = 'http://localhost:3000/api'
3565
const { ofetch } = await import('ofetch')
36-
vi.mocked(ofetch).mockResolvedValueOnce({ name: 'vue-skilld', packageName: 'vue', version: '3.5.0', content: '# vue' })
66+
mockResolveAndDetail(vi.mocked(ofetch), { packageName: 'vue' })
3767

3868
const { fetchRegistrySkill } = await import('../../src/registry/client')
3969
await fetchRegistrySkill('vue')
4070

41-
expect(ofetch).toHaveBeenCalledWith('http://localhost:3000/api/skills/vue')
71+
expect(ofetch).toHaveBeenNthCalledWith(1, 'http://localhost:3000/api/skills/resolve', expect.any(Object))
72+
expect(ofetch).toHaveBeenNthCalledWith(2, 'http://localhost:3000/api/skills/antfu/skills/vue')
4273
})
4374

4475
it('strips trailing slash from override', async () => {
4576
process.env.SKILLD_REGISTRY_URL = 'http://localhost:3000/api/'
4677
const { ofetch } = await import('ofetch')
47-
vi.mocked(ofetch).mockResolvedValueOnce({ name: 'vue-skilld', packageName: 'vue', version: '3.5.0', content: '# vue' })
78+
mockResolveAndDetail(vi.mocked(ofetch), { packageName: 'vue' })
4879

4980
const { fetchRegistrySkill } = await import('../../src/registry/client')
5081
await fetchRegistrySkill('vue')
5182

52-
expect(ofetch).toHaveBeenCalledWith('http://localhost:3000/api/skills/vue')
83+
expect(ofetch).toHaveBeenNthCalledWith(1, 'http://localhost:3000/api/skills/resolve', expect.any(Object))
84+
})
85+
86+
it('returns null when resolve fails', async () => {
87+
const { ofetch } = await import('ofetch')
88+
vi.mocked(ofetch).mockRejectedValueOnce(new Error('network'))
89+
90+
const { fetchRegistrySkill } = await import('../../src/registry/client')
91+
expect(await fetchRegistrySkill('nonexistent')).toBeNull()
5392
})
5493

55-
it('returns null when registry fetch fails', async () => {
94+
it('returns null when resolve has no hit', async () => {
5695
const { ofetch } = await import('ofetch')
57-
vi.mocked(ofetch).mockRejectedValueOnce(new Error('404'))
96+
vi.mocked(ofetch).mockResolvedValueOnce({})
5897

5998
const { fetchRegistrySkill } = await import('../../src/registry/client')
6099
expect(await fetchRegistrySkill('nonexistent')).toBeNull()
100+
expect(ofetch).toHaveBeenCalledTimes(1)
61101
})
62102

63-
it('encodes scoped package names', async () => {
103+
it('returns null when detail has no raw SKILL.md', async () => {
104+
const { ofetch } = await import('ofetch')
105+
vi.mocked(ofetch)
106+
.mockResolvedValueOnce({ vue: { owner: 'antfu', repo: 'skills', official: false } })
107+
.mockResolvedValueOnce({ owner: 'antfu', repo: 'skills', name: 'vue', raw: null })
108+
109+
const { fetchRegistrySkill } = await import('../../src/registry/client')
110+
expect(await fetchRegistrySkill('vue')).toBeNull()
111+
})
112+
113+
it('passes scoped package names unencoded to resolve body', async () => {
64114
delete process.env.SKILLD_REGISTRY_URL
65115
const { ofetch } = await import('ofetch')
66-
vi.mocked(ofetch).mockResolvedValueOnce({ name: 'nuxt-ui-skilld', packageName: '@nuxt/ui', version: '3.0.0', content: '# nuxt/ui' })
116+
mockResolveAndDetail(vi.mocked(ofetch), { packageName: '@nuxt/ui', owner: 'nuxt', repo: 'ui' })
67117

68118
const { fetchRegistrySkill } = await import('../../src/registry/client')
69119
await fetchRegistrySkill('@nuxt/ui')
70120

71-
expect(ofetch).toHaveBeenCalledWith('https://skilld.dev/api/skills/%40nuxt%2Fui')
121+
expect(ofetch).toHaveBeenNthCalledWith(1, 'https://skilld.dev/api/skills/resolve', {
122+
method: 'POST',
123+
body: { items: [{ packageName: '@nuxt/ui', owner: undefined }] },
124+
})
125+
expect(ofetch).toHaveBeenNthCalledWith(2, 'https://skilld.dev/api/skills/nuxt/ui/@nuxt/ui')
126+
})
127+
128+
it('maps detail payload into RegistrySkill', async () => {
129+
const { ofetch } = await import('ofetch')
130+
mockResolveAndDetail(vi.mocked(ofetch), {
131+
packageName: 'vue',
132+
owner: 'antfu',
133+
repo: 'skills',
134+
raw: '---\nname: vue\n---\n# vue',
135+
pushedAt: '2026-03-16T06:16:24Z',
136+
})
137+
138+
const { fetchRegistrySkill } = await import('../../src/registry/client')
139+
const skill = await fetchRegistrySkill('vue')
140+
141+
expect(skill).toMatchObject({
142+
name: 'vue',
143+
packageName: 'vue',
144+
owner: 'antfu',
145+
repo: 'antfu/skills',
146+
content: '---\nname: vue\n---\n# vue',
147+
updatedAt: '2026-03-16T06:16:24Z',
148+
branch: 'main',
149+
})
72150
})
73151
})

0 commit comments

Comments
 (0)