pilothub 0.0.1
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.
- package/.env.local.example +19 -0
- package/.github/workflows/ci.yml +40 -0
- package/.oxlintrc.json +3 -0
- package/AGENTS.md +45 -0
- package/CHANGELOG.md +138 -0
- package/DEPRECATIONS.md +7 -0
- package/LICENSE +21 -0
- package/README.md +150 -0
- package/biome.json +41 -0
- package/convex/_generated/api.d.ts +153 -0
- package/convex/_generated/api.js +23 -0
- package/convex/_generated/dataModel.d.ts +60 -0
- package/convex/_generated/server.d.ts +143 -0
- package/convex/_generated/server.js +93 -0
- package/convex/auth.config.ts +8 -0
- package/convex/auth.ts +19 -0
- package/convex/comments.ts +88 -0
- package/convex/crons.ts +34 -0
- package/convex/devSeed.ts +459 -0
- package/convex/devSeedExtra.ts +541 -0
- package/convex/downloads.ts +78 -0
- package/convex/githubBackups.ts +170 -0
- package/convex/githubBackupsNode.ts +183 -0
- package/convex/githubImport.ts +317 -0
- package/convex/githubSoulBackups.ts +170 -0
- package/convex/githubSoulBackupsNode.ts +186 -0
- package/convex/http.ts +194 -0
- package/convex/httpApi.handlers.test.ts +488 -0
- package/convex/httpApi.test.ts +70 -0
- package/convex/httpApi.ts +305 -0
- package/convex/httpApiV1.handlers.test.ts +584 -0
- package/convex/httpApiV1.ts +1172 -0
- package/convex/leaderboards.ts +39 -0
- package/convex/lib/access.ts +36 -0
- package/convex/lib/apiTokenAuth.ts +36 -0
- package/convex/lib/badges.ts +50 -0
- package/convex/lib/changelog.test.ts +34 -0
- package/convex/lib/changelog.ts +278 -0
- package/convex/lib/embeddings.ts +38 -0
- package/convex/lib/githubBackup.ts +443 -0
- package/convex/lib/githubImport.test.ts +247 -0
- package/convex/lib/githubImport.ts +425 -0
- package/convex/lib/githubSoulBackup.ts +443 -0
- package/convex/lib/leaderboards.ts +103 -0
- package/convex/lib/moderation.ts +42 -0
- package/convex/lib/public.ts +89 -0
- package/convex/lib/searchText.test.ts +46 -0
- package/convex/lib/searchText.ts +27 -0
- package/convex/lib/skillBackfill.test.ts +34 -0
- package/convex/lib/skillBackfill.ts +67 -0
- package/convex/lib/skillPublish.test.ts +28 -0
- package/convex/lib/skillPublish.ts +284 -0
- package/convex/lib/skillStats.ts +80 -0
- package/convex/lib/skills.test.ts +197 -0
- package/convex/lib/skills.ts +273 -0
- package/convex/lib/soulChangelog.ts +273 -0
- package/convex/lib/soulPublish.ts +236 -0
- package/convex/lib/tokens.test.ts +33 -0
- package/convex/lib/tokens.ts +51 -0
- package/convex/lib/webhooks.test.ts +91 -0
- package/convex/lib/webhooks.ts +112 -0
- package/convex/maintenance.test.ts +270 -0
- package/convex/maintenance.ts +840 -0
- package/convex/rateLimits.ts +50 -0
- package/convex/schema.ts +472 -0
- package/convex/search.test.ts +12 -0
- package/convex/search.ts +254 -0
- package/convex/seed.test.ts +37 -0
- package/convex/seed.ts +254 -0
- package/convex/seedSouls.ts +111 -0
- package/convex/skillStatEvents.ts +568 -0
- package/convex/skills.ts +1606 -0
- package/convex/soulComments.ts +88 -0
- package/convex/soulDownloads.ts +14 -0
- package/convex/soulStars.ts +71 -0
- package/convex/souls.ts +570 -0
- package/convex/stars.ts +108 -0
- package/convex/statsMaintenance.ts +205 -0
- package/convex/telemetry.ts +434 -0
- package/convex/tokens.ts +88 -0
- package/convex/tsconfig.json +7 -0
- package/convex/uploads.ts +20 -0
- package/convex/users.ts +122 -0
- package/convex/webhooks.ts +50 -0
- package/convex.json +3 -0
- package/docs/README.md +32 -0
- package/docs/api.md +51 -0
- package/docs/architecture.md +61 -0
- package/docs/auth.md +54 -0
- package/docs/cli.md +117 -0
- package/docs/deploy.md +78 -0
- package/docs/diffing.md +84 -0
- package/docs/github-import.md +171 -0
- package/docs/http-api.md +187 -0
- package/docs/manual-testing.md +64 -0
- package/docs/mintlify.md +43 -0
- package/docs/quickstart.md +120 -0
- package/docs/skill-format.md +58 -0
- package/docs/soul-format.md +37 -0
- package/docs/spec.md +177 -0
- package/docs/telemetry.md +91 -0
- package/docs/troubleshooting.md +49 -0
- package/docs/webhook.md +51 -0
- package/e2e/menu-smoke.pw.test.ts +49 -0
- package/e2e/pilothub.e2e.test.ts +494 -0
- package/e2e/search-exact.pw.test.ts +97 -0
- package/package.json +84 -0
- package/packages/pilothub/LICENSE +22 -0
- package/packages/pilothub/README.md +57 -0
- package/packages/pilothub/bin/pilothub.js +2 -0
- package/packages/pilothub/package.json +41 -0
- package/packages/pilothub/src/browserAuth.test.ts +96 -0
- package/packages/pilothub/src/browserAuth.ts +174 -0
- package/packages/pilothub/src/cli/buildInfo.ts +94 -0
- package/packages/pilothub/src/cli/commands/auth.ts +97 -0
- package/packages/pilothub/src/cli/commands/delete.test.ts +73 -0
- package/packages/pilothub/src/cli/commands/delete.ts +83 -0
- package/packages/pilothub/src/cli/commands/publish.test.ts +122 -0
- package/packages/pilothub/src/cli/commands/publish.ts +108 -0
- package/packages/pilothub/src/cli/commands/skills.test.ts +191 -0
- package/packages/pilothub/src/cli/commands/skills.ts +380 -0
- package/packages/pilothub/src/cli/commands/star.ts +46 -0
- package/packages/pilothub/src/cli/commands/sync.test.ts +310 -0
- package/packages/pilothub/src/cli/commands/sync.ts +200 -0
- package/packages/pilothub/src/cli/commands/syncHelpers.test.ts +26 -0
- package/packages/pilothub/src/cli/commands/syncHelpers.ts +427 -0
- package/packages/pilothub/src/cli/commands/syncTypes.ts +27 -0
- package/packages/pilothub/src/cli/commands/unstar.ts +48 -0
- package/packages/pilothub/src/cli/helpStyle.ts +45 -0
- package/packages/pilothub/src/cli/pilotbotConfig.test.ts +159 -0
- package/packages/pilothub/src/cli/pilotbotConfig.ts +147 -0
- package/packages/pilothub/src/cli/registry.test.ts +63 -0
- package/packages/pilothub/src/cli/registry.ts +43 -0
- package/packages/pilothub/src/cli/scanSkills.test.ts +64 -0
- package/packages/pilothub/src/cli/scanSkills.ts +84 -0
- package/packages/pilothub/src/cli/slug.ts +16 -0
- package/packages/pilothub/src/cli/types.ts +12 -0
- package/packages/pilothub/src/cli/ui.ts +75 -0
- package/packages/pilothub/src/cli.ts +311 -0
- package/packages/pilothub/src/config.ts +36 -0
- package/packages/pilothub/src/discovery.test.ts +75 -0
- package/packages/pilothub/src/discovery.ts +19 -0
- package/packages/pilothub/src/http.test.ts +156 -0
- package/packages/pilothub/src/http.ts +301 -0
- package/packages/pilothub/src/schema/ark.ts +29 -0
- package/packages/pilothub/src/schema/index.ts +5 -0
- package/packages/pilothub/src/schema/routes.ts +22 -0
- package/packages/pilothub/src/schema/schemas.ts +260 -0
- package/packages/pilothub/src/schema/textFiles.test.ts +23 -0
- package/packages/pilothub/src/schema/textFiles.ts +66 -0
- package/packages/pilothub/src/skills.test.ts +191 -0
- package/packages/pilothub/src/skills.ts +172 -0
- package/packages/pilothub/src/types.ts +10 -0
- package/packages/pilothub/tsconfig.json +14 -0
- package/packages/schema/README.md +3 -0
- package/packages/schema/dist/ark.d.ts +4 -0
- package/packages/schema/dist/ark.js +26 -0
- package/packages/schema/dist/ark.js.map +1 -0
- package/packages/schema/dist/index.d.ts +5 -0
- package/packages/schema/dist/index.js +5 -0
- package/packages/schema/dist/index.js.map +1 -0
- package/packages/schema/dist/routes.d.ts +21 -0
- package/packages/schema/dist/routes.js +22 -0
- package/packages/schema/dist/routes.js.map +1 -0
- package/packages/schema/dist/schemas.d.ts +297 -0
- package/packages/schema/dist/schemas.js +243 -0
- package/packages/schema/dist/schemas.js.map +1 -0
- package/packages/schema/dist/textFiles.d.ts +5 -0
- package/packages/schema/dist/textFiles.js +66 -0
- package/packages/schema/dist/textFiles.js.map +1 -0
- package/packages/schema/package.json +26 -0
- package/packages/schema/src/ark.ts +29 -0
- package/packages/schema/src/index.ts +5 -0
- package/packages/schema/src/routes.ts +22 -0
- package/packages/schema/src/schemas.test.ts +123 -0
- package/packages/schema/src/schemas.ts +287 -0
- package/packages/schema/src/textFiles.test.ts +23 -0
- package/packages/schema/src/textFiles.ts +66 -0
- package/packages/schema/tsconfig.json +15 -0
- package/pilothub +46 -0
- package/playwright.config.ts +33 -0
- package/public/.well-known/pilothub.json +6 -0
- package/public/api/v1/openapi.json +379 -0
- package/public/favicon.ico +0 -0
- package/public/logo192.png +0 -0
- package/public/logo512.png +0 -0
- package/public/manifest.json +25 -0
- package/public/og.png +0 -0
- package/public/og.svg +98 -0
- package/public/pilot-logo.png +0 -0
- package/public/pilot-mark.png +0 -0
- package/public/robots.txt +3 -0
- package/public/tanstack-circle-logo.png +0 -0
- package/public/tanstack-word-logo-white.svg +1 -0
- package/scripts/check-peer-deps.ts +56 -0
- package/scripts/docs-list.ts +148 -0
- package/scripts/run-playwright-local.sh +14 -0
- package/server/og/fetchSkillOgMeta.ts +27 -0
- package/server/og/fetchSoulOgMeta.ts +27 -0
- package/server/og/ogAssets.ts +80 -0
- package/server/og/skillOgSvg.test.ts +59 -0
- package/server/og/skillOgSvg.ts +258 -0
- package/server/og/soulOgSvg.ts +209 -0
- package/server/routes/og/skill.png.ts +103 -0
- package/server/routes/og/soul.png.ts +111 -0
- package/src/__tests__/skill-detail-page.test.tsx +86 -0
- package/src/__tests__/skills-index.test.tsx +145 -0
- package/src/__tests__/upload.route.test.tsx +228 -0
- package/src/components/AppProviders.tsx +19 -0
- package/src/components/ClientOnly.tsx +18 -0
- package/src/components/Footer.tsx +29 -0
- package/src/components/Header.tsx +295 -0
- package/src/components/InstallSwitcher.tsx +53 -0
- package/src/components/SkillCard.tsx +36 -0
- package/src/components/SkillDetailPage.tsx +817 -0
- package/src/components/SkillDiffCard.tsx +485 -0
- package/src/components/SoulCard.tsx +19 -0
- package/src/components/SoulDetailPage.tsx +263 -0
- package/src/components/UserBootstrap.tsx +18 -0
- package/src/components/ui/dropdown-menu.tsx +67 -0
- package/src/components/ui/toggle-group.tsx +35 -0
- package/src/convex/client.ts +3 -0
- package/src/lib/badges.ts +29 -0
- package/src/lib/diffing.test.ts +163 -0
- package/src/lib/diffing.ts +106 -0
- package/src/lib/gravatar.test.ts +9 -0
- package/src/lib/gravatar.ts +158 -0
- package/src/lib/og.test.ts +142 -0
- package/src/lib/og.ts +156 -0
- package/src/lib/publicUser.ts +39 -0
- package/src/lib/roles.ts +19 -0
- package/src/lib/site.test.ts +130 -0
- package/src/lib/site.ts +84 -0
- package/src/lib/theme-transition.test.ts +134 -0
- package/src/lib/theme-transition.ts +134 -0
- package/src/lib/theme.test.tsx +88 -0
- package/src/lib/theme.ts +43 -0
- package/src/lib/uploadFiles.jsdom.test.ts +33 -0
- package/src/lib/uploadFiles.test.ts +123 -0
- package/src/lib/uploadFiles.ts +245 -0
- package/src/lib/uploadUtils.test.ts +78 -0
- package/src/lib/uploadUtils.ts +93 -0
- package/src/lib/useAuthStatus.ts +12 -0
- package/src/lib/utils.test.ts +9 -0
- package/src/lib/utils.ts +6 -0
- package/src/logo.svg +12 -0
- package/src/routeTree.gen.ts +345 -0
- package/src/router.tsx +17 -0
- package/src/routes/$owner/$slug.tsx +55 -0
- package/src/routes/__root.tsx +136 -0
- package/src/routes/admin.tsx +11 -0
- package/src/routes/cli/auth.tsx +168 -0
- package/src/routes/dashboard.tsx +97 -0
- package/src/routes/import.tsx +415 -0
- package/src/routes/index.tsx +252 -0
- package/src/routes/management.tsx +529 -0
- package/src/routes/settings.tsx +203 -0
- package/src/routes/skills/index.tsx +422 -0
- package/src/routes/souls/$slug.tsx +55 -0
- package/src/routes/souls/index.tsx +243 -0
- package/src/routes/stars.tsx +68 -0
- package/src/routes/u/$handle.tsx +307 -0
- package/src/routes/upload/utils.ts +81 -0
- package/src/routes/upload.tsx +499 -0
- package/src/styles.css +2718 -0
- package/tsconfig.json +24 -0
- package/tsconfig.oxlint.json +16 -0
- package/vercel.json +8 -0
- package/vite.config.ts +48 -0
- package/vitest.config.ts +47 -0
- package/vitest.e2e.config.ts +11 -0
- package/vitest.setup.ts +1 -0
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
/* @vitest-environment node */
|
|
2
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
|
|
4
|
+
vi.mock('./lib/apiTokenAuth', () => ({
|
|
5
|
+
requireApiTokenUser: vi.fn(),
|
|
6
|
+
}))
|
|
7
|
+
|
|
8
|
+
vi.mock('./skills', () => ({
|
|
9
|
+
publishVersionForUser: vi.fn(),
|
|
10
|
+
}))
|
|
11
|
+
|
|
12
|
+
const { requireApiTokenUser } = await import('./lib/apiTokenAuth')
|
|
13
|
+
const { publishVersionForUser } = await import('./skills')
|
|
14
|
+
const { __handlers } = await import('./httpApiV1')
|
|
15
|
+
|
|
16
|
+
type ActionCtx = import('./_generated/server').ActionCtx
|
|
17
|
+
|
|
18
|
+
function makeCtx(partial: Record<string, unknown>) {
|
|
19
|
+
return partial as unknown as ActionCtx
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const okRate = () => ({
|
|
23
|
+
allowed: true,
|
|
24
|
+
remaining: 10,
|
|
25
|
+
limit: 100,
|
|
26
|
+
resetAt: Date.now() + 60_000,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
const blockedRate = () => ({
|
|
30
|
+
allowed: false,
|
|
31
|
+
remaining: 0,
|
|
32
|
+
limit: 100,
|
|
33
|
+
resetAt: Date.now() + 60_000,
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.mocked(requireApiTokenUser).mockReset()
|
|
38
|
+
vi.mocked(publishVersionForUser).mockReset()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
describe('httpApiV1 handlers', () => {
|
|
42
|
+
it('search returns empty results for blank query', async () => {
|
|
43
|
+
const runAction = vi.fn()
|
|
44
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
45
|
+
const response = await __handlers.searchSkillsV1Handler(
|
|
46
|
+
makeCtx({ runAction, runMutation }),
|
|
47
|
+
new Request('https://example.com/api/v1/search?q=%20%20'),
|
|
48
|
+
)
|
|
49
|
+
if (response.status !== 200) {
|
|
50
|
+
throw new Error(await response.text())
|
|
51
|
+
}
|
|
52
|
+
expect(await response.json()).toEqual({ results: [] })
|
|
53
|
+
expect(runAction).not.toHaveBeenCalled()
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('search forwards limit and highlightedOnly', async () => {
|
|
57
|
+
const runAction = vi.fn().mockResolvedValue([
|
|
58
|
+
{
|
|
59
|
+
score: 1,
|
|
60
|
+
skill: { slug: 'a', displayName: 'A', summary: null, updatedAt: 1 },
|
|
61
|
+
version: { version: '1.0.0' },
|
|
62
|
+
},
|
|
63
|
+
])
|
|
64
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
65
|
+
const response = await __handlers.searchSkillsV1Handler(
|
|
66
|
+
makeCtx({ runAction, runMutation }),
|
|
67
|
+
new Request('https://example.com/api/v1/search?q=test&limit=5&highlightedOnly=true'),
|
|
68
|
+
)
|
|
69
|
+
if (response.status !== 200) {
|
|
70
|
+
throw new Error(await response.text())
|
|
71
|
+
}
|
|
72
|
+
expect(runAction).toHaveBeenCalledWith(expect.anything(), {
|
|
73
|
+
query: 'test',
|
|
74
|
+
limit: 5,
|
|
75
|
+
highlightedOnly: true,
|
|
76
|
+
})
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('search rate limits', async () => {
|
|
80
|
+
const runMutation = vi.fn().mockResolvedValue(blockedRate())
|
|
81
|
+
const response = await __handlers.searchSkillsV1Handler(
|
|
82
|
+
makeCtx({ runAction: vi.fn(), runMutation }),
|
|
83
|
+
new Request('https://example.com/api/v1/search?q=test'),
|
|
84
|
+
)
|
|
85
|
+
expect(response.status).toBe(429)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('resolve validates hash', async () => {
|
|
89
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
90
|
+
const response = await __handlers.resolveSkillVersionV1Handler(
|
|
91
|
+
makeCtx({ runQuery: vi.fn(), runMutation }),
|
|
92
|
+
new Request('https://example.com/api/v1/resolve?slug=demo&hash=bad'),
|
|
93
|
+
)
|
|
94
|
+
expect(response.status).toBe(400)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('resolve returns 404 when missing', async () => {
|
|
98
|
+
const runQuery = vi.fn().mockResolvedValue(null)
|
|
99
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
100
|
+
const response = await __handlers.resolveSkillVersionV1Handler(
|
|
101
|
+
makeCtx({ runQuery, runMutation }),
|
|
102
|
+
new Request(
|
|
103
|
+
'https://example.com/api/v1/resolve?slug=demo&hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
104
|
+
),
|
|
105
|
+
)
|
|
106
|
+
expect(response.status).toBe(404)
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('resolve returns match and latestVersion', async () => {
|
|
110
|
+
const runQuery = vi.fn().mockResolvedValue({
|
|
111
|
+
match: { version: '1.0.0' },
|
|
112
|
+
latestVersion: { version: '2.0.0' },
|
|
113
|
+
})
|
|
114
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
115
|
+
const response = await __handlers.resolveSkillVersionV1Handler(
|
|
116
|
+
makeCtx({ runQuery, runMutation }),
|
|
117
|
+
new Request(
|
|
118
|
+
'https://example.com/api/v1/resolve?slug=demo&hash=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
|
|
119
|
+
),
|
|
120
|
+
)
|
|
121
|
+
expect(response.status).toBe(200)
|
|
122
|
+
const json = await response.json()
|
|
123
|
+
expect(json.match.version).toBe('1.0.0')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('lists skills with resolved tags', async () => {
|
|
127
|
+
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
|
128
|
+
if ('cursor' in args || 'limit' in args) {
|
|
129
|
+
return {
|
|
130
|
+
items: [
|
|
131
|
+
{
|
|
132
|
+
skill: {
|
|
133
|
+
_id: 'skills:1',
|
|
134
|
+
slug: 'demo',
|
|
135
|
+
displayName: 'Demo',
|
|
136
|
+
summary: 's',
|
|
137
|
+
tags: { latest: 'versions:1' },
|
|
138
|
+
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
|
|
139
|
+
createdAt: 1,
|
|
140
|
+
updatedAt: 2,
|
|
141
|
+
},
|
|
142
|
+
latestVersion: { version: '1.0.0', createdAt: 3, changelog: 'c' },
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
nextCursor: null,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if ('versionId' in args) return { version: '1.0.0' }
|
|
149
|
+
return null
|
|
150
|
+
})
|
|
151
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
152
|
+
const response = await __handlers.listSkillsV1Handler(
|
|
153
|
+
makeCtx({ runQuery, runMutation }),
|
|
154
|
+
new Request('https://example.com/api/v1/skills?limit=1'),
|
|
155
|
+
)
|
|
156
|
+
expect(response.status).toBe(200)
|
|
157
|
+
const json = await response.json()
|
|
158
|
+
expect(json.items[0].tags.latest).toBe('1.0.0')
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('lists skills supports sort aliases', async () => {
|
|
162
|
+
const checks: Array<[string, string]> = [
|
|
163
|
+
['rating', 'stars'],
|
|
164
|
+
['installs', 'installsCurrent'],
|
|
165
|
+
['installs-all-time', 'installsAllTime'],
|
|
166
|
+
['trending', 'trending'],
|
|
167
|
+
]
|
|
168
|
+
|
|
169
|
+
for (const [input, expected] of checks) {
|
|
170
|
+
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
|
171
|
+
if ('sort' in args || 'cursor' in args || 'limit' in args) {
|
|
172
|
+
expect(args.sort).toBe(expected)
|
|
173
|
+
return { items: [], nextCursor: null }
|
|
174
|
+
}
|
|
175
|
+
return null
|
|
176
|
+
})
|
|
177
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
178
|
+
const response = await __handlers.listSkillsV1Handler(
|
|
179
|
+
makeCtx({ runQuery, runMutation }),
|
|
180
|
+
new Request(`https://example.com/api/v1/skills?sort=${input}`),
|
|
181
|
+
)
|
|
182
|
+
expect(response.status).toBe(200)
|
|
183
|
+
}
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('get skill returns 404 when missing', async () => {
|
|
187
|
+
const runQuery = vi.fn().mockResolvedValue(null)
|
|
188
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
189
|
+
const response = await __handlers.skillsGetRouterV1Handler(
|
|
190
|
+
makeCtx({ runQuery, runMutation }),
|
|
191
|
+
new Request('https://example.com/api/v1/skills/missing'),
|
|
192
|
+
)
|
|
193
|
+
expect(response.status).toBe(404)
|
|
194
|
+
})
|
|
195
|
+
|
|
196
|
+
it('get skill returns payload', async () => {
|
|
197
|
+
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
|
198
|
+
if ('slug' in args) {
|
|
199
|
+
return {
|
|
200
|
+
skill: {
|
|
201
|
+
_id: 'skills:1',
|
|
202
|
+
slug: 'demo',
|
|
203
|
+
displayName: 'Demo',
|
|
204
|
+
summary: 's',
|
|
205
|
+
tags: { latest: 'versions:1' },
|
|
206
|
+
stats: { downloads: 0, stars: 0, versions: 1, comments: 0 },
|
|
207
|
+
createdAt: 1,
|
|
208
|
+
updatedAt: 2,
|
|
209
|
+
},
|
|
210
|
+
latestVersion: {
|
|
211
|
+
version: '1.0.0',
|
|
212
|
+
createdAt: 3,
|
|
213
|
+
changelog: 'c',
|
|
214
|
+
files: [],
|
|
215
|
+
},
|
|
216
|
+
owner: { handle: 'p', displayName: 'Peter', image: null },
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if ('versionId' in args) return { version: '1.0.0' }
|
|
220
|
+
return null
|
|
221
|
+
})
|
|
222
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
223
|
+
const response = await __handlers.skillsGetRouterV1Handler(
|
|
224
|
+
makeCtx({ runQuery, runMutation }),
|
|
225
|
+
new Request('https://example.com/api/v1/skills/demo'),
|
|
226
|
+
)
|
|
227
|
+
expect(response.status).toBe(200)
|
|
228
|
+
const json = await response.json()
|
|
229
|
+
expect(json.skill.slug).toBe('demo')
|
|
230
|
+
expect(json.latestVersion.version).toBe('1.0.0')
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
it('lists versions', async () => {
|
|
234
|
+
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
|
235
|
+
if ('slug' in args) {
|
|
236
|
+
return { _id: 'skills:1', slug: 'demo', displayName: 'Demo' }
|
|
237
|
+
}
|
|
238
|
+
if ('skillId' in args && 'cursor' in args) {
|
|
239
|
+
return {
|
|
240
|
+
items: [
|
|
241
|
+
{
|
|
242
|
+
version: '1.0.0',
|
|
243
|
+
createdAt: 1,
|
|
244
|
+
changelog: 'c',
|
|
245
|
+
changelogSource: 'user',
|
|
246
|
+
files: [],
|
|
247
|
+
},
|
|
248
|
+
],
|
|
249
|
+
nextCursor: null,
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return null
|
|
253
|
+
})
|
|
254
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
255
|
+
const response = await __handlers.skillsGetRouterV1Handler(
|
|
256
|
+
makeCtx({ runQuery, runMutation }),
|
|
257
|
+
new Request('https://example.com/api/v1/skills/demo/versions?limit=1'),
|
|
258
|
+
)
|
|
259
|
+
expect(response.status).toBe(200)
|
|
260
|
+
const json = await response.json()
|
|
261
|
+
expect(json.items[0].version).toBe('1.0.0')
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
it('returns version detail', async () => {
|
|
265
|
+
const runQuery = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
|
266
|
+
if ('slug' in args) {
|
|
267
|
+
return { _id: 'skills:1', slug: 'demo', displayName: 'Demo' }
|
|
268
|
+
}
|
|
269
|
+
if ('skillId' in args && 'version' in args) {
|
|
270
|
+
return {
|
|
271
|
+
version: '1.0.0',
|
|
272
|
+
createdAt: 1,
|
|
273
|
+
changelog: 'c',
|
|
274
|
+
changelogSource: 'auto',
|
|
275
|
+
files: [
|
|
276
|
+
{
|
|
277
|
+
path: 'SKILL.md',
|
|
278
|
+
size: 1,
|
|
279
|
+
storageId: 'storage:1',
|
|
280
|
+
sha256: 'abc',
|
|
281
|
+
contentType: 'text/plain',
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
return null
|
|
287
|
+
})
|
|
288
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
289
|
+
const response = await __handlers.skillsGetRouterV1Handler(
|
|
290
|
+
makeCtx({ runQuery, runMutation }),
|
|
291
|
+
new Request('https://example.com/api/v1/skills/demo/versions/1.0.0'),
|
|
292
|
+
)
|
|
293
|
+
expect(response.status).toBe(200)
|
|
294
|
+
const json = await response.json()
|
|
295
|
+
expect(json.version.files[0].path).toBe('SKILL.md')
|
|
296
|
+
})
|
|
297
|
+
|
|
298
|
+
it('returns raw file content', async () => {
|
|
299
|
+
const version = {
|
|
300
|
+
version: '1.0.0',
|
|
301
|
+
createdAt: 1,
|
|
302
|
+
changelog: 'c',
|
|
303
|
+
files: [
|
|
304
|
+
{
|
|
305
|
+
path: 'SKILL.md',
|
|
306
|
+
size: 5,
|
|
307
|
+
storageId: 'storage:1',
|
|
308
|
+
sha256: 'abcd',
|
|
309
|
+
contentType: 'text/plain',
|
|
310
|
+
},
|
|
311
|
+
],
|
|
312
|
+
softDeletedAt: undefined,
|
|
313
|
+
}
|
|
314
|
+
const runQuery = vi.fn().mockResolvedValue({
|
|
315
|
+
skill: {
|
|
316
|
+
_id: 'skills:1',
|
|
317
|
+
slug: 'demo',
|
|
318
|
+
displayName: 'Demo',
|
|
319
|
+
summary: 's',
|
|
320
|
+
tags: {},
|
|
321
|
+
stats: {},
|
|
322
|
+
createdAt: 1,
|
|
323
|
+
updatedAt: 2,
|
|
324
|
+
},
|
|
325
|
+
latestVersion: version,
|
|
326
|
+
owner: null,
|
|
327
|
+
})
|
|
328
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
329
|
+
const storage = {
|
|
330
|
+
get: vi.fn().mockResolvedValue(new Blob(['hello'], { type: 'text/plain' })),
|
|
331
|
+
}
|
|
332
|
+
const response = await __handlers.skillsGetRouterV1Handler(
|
|
333
|
+
makeCtx({ runQuery, runMutation, storage }),
|
|
334
|
+
new Request('https://example.com/api/v1/skills/demo/file?path=SKILL.md'),
|
|
335
|
+
)
|
|
336
|
+
expect(response.status).toBe(200)
|
|
337
|
+
expect(await response.text()).toBe('hello')
|
|
338
|
+
expect(response.headers.get('X-Content-SHA256')).toBe('abcd')
|
|
339
|
+
})
|
|
340
|
+
|
|
341
|
+
it('returns 413 when raw file too large', async () => {
|
|
342
|
+
const version = {
|
|
343
|
+
version: '1.0.0',
|
|
344
|
+
createdAt: 1,
|
|
345
|
+
changelog: 'c',
|
|
346
|
+
files: [
|
|
347
|
+
{
|
|
348
|
+
path: 'SKILL.md',
|
|
349
|
+
size: 210 * 1024,
|
|
350
|
+
storageId: 'storage:1',
|
|
351
|
+
sha256: 'abcd',
|
|
352
|
+
contentType: 'text/plain',
|
|
353
|
+
},
|
|
354
|
+
],
|
|
355
|
+
softDeletedAt: undefined,
|
|
356
|
+
}
|
|
357
|
+
const runQuery = vi.fn().mockResolvedValue({
|
|
358
|
+
skill: {
|
|
359
|
+
_id: 'skills:1',
|
|
360
|
+
slug: 'demo',
|
|
361
|
+
displayName: 'Demo',
|
|
362
|
+
summary: 's',
|
|
363
|
+
tags: {},
|
|
364
|
+
stats: {},
|
|
365
|
+
createdAt: 1,
|
|
366
|
+
updatedAt: 2,
|
|
367
|
+
},
|
|
368
|
+
latestVersion: version,
|
|
369
|
+
owner: null,
|
|
370
|
+
})
|
|
371
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
372
|
+
const response = await __handlers.skillsGetRouterV1Handler(
|
|
373
|
+
makeCtx({ runQuery, runMutation, storage: { get: vi.fn() } }),
|
|
374
|
+
new Request('https://example.com/api/v1/skills/demo/file?path=SKILL.md'),
|
|
375
|
+
)
|
|
376
|
+
expect(response.status).toBe(413)
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('publish json succeeds', async () => {
|
|
380
|
+
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
|
|
381
|
+
userId: 'users:1',
|
|
382
|
+
user: { handle: 'p' },
|
|
383
|
+
} as never)
|
|
384
|
+
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
|
|
385
|
+
skillId: 's',
|
|
386
|
+
versionId: 'v',
|
|
387
|
+
embeddingId: 'e',
|
|
388
|
+
} as never)
|
|
389
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
390
|
+
const body = JSON.stringify({
|
|
391
|
+
slug: 'demo',
|
|
392
|
+
displayName: 'Demo',
|
|
393
|
+
version: '1.0.0',
|
|
394
|
+
changelog: 'c',
|
|
395
|
+
files: [
|
|
396
|
+
{
|
|
397
|
+
path: 'SKILL.md',
|
|
398
|
+
size: 1,
|
|
399
|
+
storageId: 'storage:1',
|
|
400
|
+
sha256: 'abc',
|
|
401
|
+
contentType: 'text/plain',
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
})
|
|
405
|
+
const response = await __handlers.publishSkillV1Handler(
|
|
406
|
+
makeCtx({ runMutation }),
|
|
407
|
+
new Request('https://example.com/api/v1/skills', {
|
|
408
|
+
method: 'POST',
|
|
409
|
+
headers: { 'Content-Type': 'application/json', Authorization: 'Bearer clh_test' },
|
|
410
|
+
body,
|
|
411
|
+
}),
|
|
412
|
+
)
|
|
413
|
+
expect(response.status).toBe(200)
|
|
414
|
+
const json = await response.json()
|
|
415
|
+
expect(json.ok).toBe(true)
|
|
416
|
+
expect(publishVersionForUser).toHaveBeenCalled()
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it('publish multipart succeeds', async () => {
|
|
420
|
+
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
|
|
421
|
+
userId: 'users:1',
|
|
422
|
+
user: { handle: 'p' },
|
|
423
|
+
} as never)
|
|
424
|
+
vi.mocked(publishVersionForUser).mockResolvedValueOnce({
|
|
425
|
+
skillId: 's',
|
|
426
|
+
versionId: 'v',
|
|
427
|
+
embeddingId: 'e',
|
|
428
|
+
} as never)
|
|
429
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
430
|
+
const form = new FormData()
|
|
431
|
+
form.set(
|
|
432
|
+
'payload',
|
|
433
|
+
JSON.stringify({
|
|
434
|
+
slug: 'demo',
|
|
435
|
+
displayName: 'Demo',
|
|
436
|
+
version: '1.0.0',
|
|
437
|
+
changelog: '',
|
|
438
|
+
tags: ['latest'],
|
|
439
|
+
}),
|
|
440
|
+
)
|
|
441
|
+
form.append('files', new Blob(['hello'], { type: 'text/plain' }), 'SKILL.md')
|
|
442
|
+
const response = await __handlers.publishSkillV1Handler(
|
|
443
|
+
makeCtx({ runMutation, storage: { store: vi.fn().mockResolvedValue('storage:1') } }),
|
|
444
|
+
new Request('https://example.com/api/v1/skills', {
|
|
445
|
+
method: 'POST',
|
|
446
|
+
headers: { Authorization: 'Bearer clh_test' },
|
|
447
|
+
body: form,
|
|
448
|
+
}),
|
|
449
|
+
)
|
|
450
|
+
if (response.status !== 200) {
|
|
451
|
+
throw new Error(await response.text())
|
|
452
|
+
}
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
it('publish rejects missing token', async () => {
|
|
456
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
457
|
+
const response = await __handlers.publishSkillV1Handler(
|
|
458
|
+
makeCtx({ runMutation }),
|
|
459
|
+
new Request('https://example.com/api/v1/skills', { method: 'POST' }),
|
|
460
|
+
)
|
|
461
|
+
expect(response.status).toBe(401)
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
it('whoami returns user payload', async () => {
|
|
465
|
+
vi.mocked(requireApiTokenUser).mockResolvedValueOnce({
|
|
466
|
+
userId: 'users:1',
|
|
467
|
+
user: { handle: 'p', displayName: 'Peter', image: null },
|
|
468
|
+
} as never)
|
|
469
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
470
|
+
const response = await __handlers.whoamiV1Handler(
|
|
471
|
+
makeCtx({ runMutation }),
|
|
472
|
+
new Request('https://example.com/api/v1/whoami', {
|
|
473
|
+
headers: { Authorization: 'Bearer clh_test' },
|
|
474
|
+
}),
|
|
475
|
+
)
|
|
476
|
+
expect(response.status).toBe(200)
|
|
477
|
+
const json = await response.json()
|
|
478
|
+
expect(json.user.handle).toBe('p')
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
it('delete and undelete require auth', async () => {
|
|
482
|
+
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error('Unauthorized'))
|
|
483
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
484
|
+
const response = await __handlers.skillsDeleteRouterV1Handler(
|
|
485
|
+
makeCtx({ runMutation }),
|
|
486
|
+
new Request('https://example.com/api/v1/skills/demo', { method: 'DELETE' }),
|
|
487
|
+
)
|
|
488
|
+
expect(response.status).toBe(401)
|
|
489
|
+
|
|
490
|
+
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error('Unauthorized'))
|
|
491
|
+
const response2 = await __handlers.skillsPostRouterV1Handler(
|
|
492
|
+
makeCtx({ runMutation }),
|
|
493
|
+
new Request('https://example.com/api/v1/skills/demo/undelete', { method: 'POST' }),
|
|
494
|
+
)
|
|
495
|
+
expect(response2.status).toBe(401)
|
|
496
|
+
})
|
|
497
|
+
|
|
498
|
+
it('delete and undelete succeed', async () => {
|
|
499
|
+
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
|
500
|
+
userId: 'users:1',
|
|
501
|
+
user: { handle: 'p' },
|
|
502
|
+
} as never)
|
|
503
|
+
const runMutation = vi.fn(async (_query: unknown, args: Record<string, unknown>) => {
|
|
504
|
+
if ('key' in args) return okRate()
|
|
505
|
+
return { ok: true }
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
const response = await __handlers.skillsDeleteRouterV1Handler(
|
|
509
|
+
makeCtx({ runMutation }),
|
|
510
|
+
new Request('https://example.com/api/v1/skills/demo', {
|
|
511
|
+
method: 'DELETE',
|
|
512
|
+
headers: { Authorization: 'Bearer clh_test' },
|
|
513
|
+
}),
|
|
514
|
+
)
|
|
515
|
+
expect(response.status).toBe(200)
|
|
516
|
+
|
|
517
|
+
const response2 = await __handlers.skillsPostRouterV1Handler(
|
|
518
|
+
makeCtx({ runMutation }),
|
|
519
|
+
new Request('https://example.com/api/v1/skills/demo/undelete', {
|
|
520
|
+
method: 'POST',
|
|
521
|
+
headers: { Authorization: 'Bearer clh_test' },
|
|
522
|
+
}),
|
|
523
|
+
)
|
|
524
|
+
expect(response2.status).toBe(200)
|
|
525
|
+
})
|
|
526
|
+
|
|
527
|
+
it('stars require auth', async () => {
|
|
528
|
+
vi.mocked(requireApiTokenUser).mockRejectedValueOnce(new Error('Unauthorized'))
|
|
529
|
+
const runMutation = vi.fn().mockResolvedValue(okRate())
|
|
530
|
+
const response = await __handlers.starsPostRouterV1Handler(
|
|
531
|
+
makeCtx({ runMutation }),
|
|
532
|
+
new Request('https://example.com/api/v1/stars/demo', { method: 'POST' }),
|
|
533
|
+
)
|
|
534
|
+
expect(response.status).toBe(401)
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
it('stars add succeeds', async () => {
|
|
538
|
+
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
|
539
|
+
userId: 'users:1',
|
|
540
|
+
user: { handle: 'p' },
|
|
541
|
+
} as never)
|
|
542
|
+
const runQuery = vi.fn().mockResolvedValue({ _id: 'skills:1' })
|
|
543
|
+
const runMutation = vi
|
|
544
|
+
.fn()
|
|
545
|
+
.mockResolvedValueOnce(okRate())
|
|
546
|
+
.mockResolvedValueOnce(okRate())
|
|
547
|
+
.mockResolvedValueOnce({ ok: true, starred: true, alreadyStarred: false })
|
|
548
|
+
const response = await __handlers.starsPostRouterV1Handler(
|
|
549
|
+
makeCtx({ runQuery, runMutation }),
|
|
550
|
+
new Request('https://example.com/api/v1/stars/demo', {
|
|
551
|
+
method: 'POST',
|
|
552
|
+
headers: { Authorization: 'Bearer clh_test' },
|
|
553
|
+
}),
|
|
554
|
+
)
|
|
555
|
+
expect(response.status).toBe(200)
|
|
556
|
+
const json = await response.json()
|
|
557
|
+
expect(json.ok).toBe(true)
|
|
558
|
+
expect(json.starred).toBe(true)
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
it('stars delete succeeds', async () => {
|
|
562
|
+
vi.mocked(requireApiTokenUser).mockResolvedValue({
|
|
563
|
+
userId: 'users:1',
|
|
564
|
+
user: { handle: 'p' },
|
|
565
|
+
} as never)
|
|
566
|
+
const runQuery = vi.fn().mockResolvedValue({ _id: 'skills:1' })
|
|
567
|
+
const runMutation = vi
|
|
568
|
+
.fn()
|
|
569
|
+
.mockResolvedValueOnce(okRate())
|
|
570
|
+
.mockResolvedValueOnce(okRate())
|
|
571
|
+
.mockResolvedValueOnce({ ok: true, unstarred: true, alreadyUnstarred: false })
|
|
572
|
+
const response = await __handlers.starsDeleteRouterV1Handler(
|
|
573
|
+
makeCtx({ runQuery, runMutation }),
|
|
574
|
+
new Request('https://example.com/api/v1/stars/demo', {
|
|
575
|
+
method: 'DELETE',
|
|
576
|
+
headers: { Authorization: 'Bearer clh_test' },
|
|
577
|
+
}),
|
|
578
|
+
)
|
|
579
|
+
expect(response.status).toBe(200)
|
|
580
|
+
const json = await response.json()
|
|
581
|
+
expect(json.ok).toBe(true)
|
|
582
|
+
expect(json.unstarred).toBe(true)
|
|
583
|
+
})
|
|
584
|
+
})
|