test: fix lint and coverage compatibility after upgrades

This commit is contained in:
Peter Steinberger
2026-03-13 13:11:05 +00:00
parent 2486159e96
commit d2956bc64b
9 changed files with 107 additions and 75 deletions
+3 -3
View File
@@ -180,11 +180,11 @@ export async function publishVersionForUser(
)
let similarRecentCount = 0
for (const entry of recentCandidates) {
const version = (await ctx.runQuery(internal.skills.getVersionByIdInternal, {
const recentVersion = (await ctx.runQuery(internal.skills.getVersionByIdInternal, {
versionId: entry.latestVersionId as Id<'skillVersions'>,
})) as Doc<'skillVersions'> | null
if (!version) continue
const candidateReadmeFile = version.files.find((file) => {
if (!recentVersion) continue
const candidateReadmeFile = recentVersion.files.find((file) => {
const lower = file.path.toLowerCase()
return lower === 'skill.md' || lower === 'skills.md'
})
+21 -21
View File
@@ -98,14 +98,14 @@ export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) {
.filter((entry): entry is SkillInstallSpec => Boolean(entry))
const osRaw = normalizeStringList(clawdisObj.os)
const metadata: ClawdisSkillMetadata = {}
if (typeof clawdisObj.always === 'boolean') metadata.always = clawdisObj.always
if (typeof clawdisObj.emoji === 'string') metadata.emoji = clawdisObj.emoji
if (typeof clawdisObj.homepage === 'string') metadata.homepage = clawdisObj.homepage
if (typeof clawdisObj.skillKey === 'string') metadata.skillKey = clawdisObj.skillKey
if (typeof clawdisObj.primaryEnv === 'string') metadata.primaryEnv = clawdisObj.primaryEnv
if (typeof clawdisObj.cliHelp === 'string') metadata.cliHelp = clawdisObj.cliHelp
if (osRaw.length > 0) metadata.os = osRaw
const parsedMetadata: ClawdisSkillMetadata = {}
if (typeof clawdisObj.always === 'boolean') parsedMetadata.always = clawdisObj.always
if (typeof clawdisObj.emoji === 'string') parsedMetadata.emoji = clawdisObj.emoji
if (typeof clawdisObj.homepage === 'string') parsedMetadata.homepage = clawdisObj.homepage
if (typeof clawdisObj.skillKey === 'string') parsedMetadata.skillKey = clawdisObj.skillKey
if (typeof clawdisObj.primaryEnv === 'string') parsedMetadata.primaryEnv = clawdisObj.primaryEnv
if (typeof clawdisObj.cliHelp === 'string') parsedMetadata.cliHelp = clawdisObj.cliHelp
if (osRaw.length > 0) parsedMetadata.os = osRaw
if (requiresRaw) {
const bins = normalizeStringList(requiresRaw.bins)
@@ -113,34 +113,34 @@ export function parseClawdisMetadata(frontmatter: ParsedSkillFrontmatter) {
const env = normalizeStringList(requiresRaw.env)
const config = normalizeStringList(requiresRaw.config)
if (bins.length || anyBins.length || env.length || config.length) {
metadata.requires = {}
if (bins.length) metadata.requires.bins = bins
if (anyBins.length) metadata.requires.anyBins = anyBins
if (env.length) metadata.requires.env = env
if (config.length) metadata.requires.config = config
parsedMetadata.requires = {}
if (bins.length) parsedMetadata.requires.bins = bins
if (anyBins.length) parsedMetadata.requires.anyBins = anyBins
if (env.length) parsedMetadata.requires.env = env
if (config.length) parsedMetadata.requires.config = config
}
}
if (install.length > 0) metadata.install = install
if (install.length > 0) parsedMetadata.install = install
const nix = parseNixPluginSpec(clawdisObj.nix)
if (nix) metadata.nix = nix
if (nix) parsedMetadata.nix = nix
const config = parseClawdbotConfigSpec(clawdisObj.config)
if (config) metadata.config = config
if (config) parsedMetadata.config = config
// Parse env var declarations (detailed env with descriptions)
const envVars = parseEnvVarDeclarations(clawdisObj.envVars ?? clawdisObj.env)
if (envVars.length > 0) metadata.envVars = envVars
if (envVars.length > 0) parsedMetadata.envVars = envVars
// Parse dependency declarations
const dependencies = parseDependencyDeclarations(clawdisObj.dependencies)
if (dependencies.length > 0) metadata.dependencies = dependencies
if (dependencies.length > 0) parsedMetadata.dependencies = dependencies
// Parse author and links
if (typeof clawdisObj.author === 'string') metadata.author = clawdisObj.author
if (typeof clawdisObj.author === 'string') parsedMetadata.author = clawdisObj.author
const links = parseSkillLinks(clawdisObj.links)
if (links) metadata.links = links
if (links) parsedMetadata.links = links
return parseArk(ClawdisSkillMetadataSchema, metadata, 'Clawdis metadata')
return parseArk(ClawdisSkillMetadataSchema, parsedMetadata, 'Clawdis metadata')
} catch {
return undefined
}
+1 -1
View File
@@ -2526,7 +2526,7 @@ export const getVersionsByIdsInternal = internalQuery({
const versions = await Promise.all(
args.versionIds.map((id) => ctx.db.get(id)),
)
return versions.filter((v): v is NonNullable<typeof v> => v !== null)
return versions.filter((versionDoc): versionDoc is NonNullable<typeof versionDoc> => versionDoc !== null)
},
})
+1 -1
View File
@@ -150,7 +150,7 @@ export const getVersionsByIdsInternal = internalQuery({
args: { versionIds: v.array(v.id('soulVersions')) },
handler: async (ctx, args) => {
const versions = await Promise.all(args.versionIds.map((id) => ctx.db.get(id)))
return versions.filter((v): v is NonNullable<typeof v> => v !== null)
return versions.filter((versionDoc): versionDoc is NonNullable<typeof versionDoc> => versionDoc !== null)
},
})
+73 -42
View File
@@ -1,23 +1,43 @@
/* @vitest-environment node */
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const originalBunVersion = (process.versions as Record<string, string | undefined>).bun
function enableBunRuntime() {
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const bunRuntimeMocks = vi.hoisted(() => {
const originalBunVersion = (process.versions as Record<string, string | undefined>).bun
Object.defineProperty(process.versions, 'bun', {
value: '1.2.3',
configurable: true,
})
}
return {
originalBunVersion,
spawnSync: vi.fn(),
mkdtemp: vi.fn(async () => '/tmp/clawhub-test'),
rm: vi.fn(async () => undefined),
writeFile: vi.fn(async () => undefined),
readFile: vi.fn(async () => Buffer.from([1, 2, 3]) as Buffer<ArrayBuffer>),
}
})
vi.mock('node:child_process', () => ({
spawnSync: bunRuntimeMocks.spawnSync,
}))
vi.mock('node:fs/promises', () => ({
mkdtemp: bunRuntimeMocks.mkdtemp,
rm: bunRuntimeMocks.rm,
writeFile: bunRuntimeMocks.writeFile,
readFile: bunRuntimeMocks.readFile,
}))
import * as http from './http'
function restoreBunRuntime() {
if (originalBunVersion === undefined) {
if (bunRuntimeMocks.originalBunVersion === undefined) {
Reflect.deleteProperty(process.versions, 'bun')
return
}
Object.defineProperty(process.versions, 'bun', {
value: originalBunVersion,
value: bunRuntimeMocks.originalBunVersion,
configurable: true,
})
}
@@ -33,50 +53,61 @@ function mockImmediateTimeouts() {
return { setTimeoutMock, clearTimeoutMock }
}
type SpawnImpl = (...args: unknown[]) => unknown
async function loadHttpModuleWithBunMocks(opts?: {
spawnImpl?: ReturnType<typeof vi.fn>
spawnImpl?: SpawnImpl
mkdtempValue?: string
readFileValue?: Buffer | null
}) {
const spawnSync = opts?.spawnImpl ?? vi.fn()
const mkdtemp = vi.fn(async () => opts?.mkdtempValue ?? '/tmp/clawhub-test')
const rm = vi.fn(async () => undefined)
const writeFile = vi.fn(async () => undefined)
const readFile = vi.fn(async () => opts?.readFileValue ?? Buffer.from([1, 2, 3]))
const spawnSync: SpawnImpl = opts?.spawnImpl ?? vi.fn()
bunRuntimeMocks.spawnSync.mockImplementation((...args: unknown[]) => spawnSync(...args))
bunRuntimeMocks.mkdtemp.mockImplementation(async () => opts?.mkdtempValue ?? '/tmp/clawhub-test')
bunRuntimeMocks.rm.mockImplementation(async () => undefined)
bunRuntimeMocks.writeFile.mockImplementation(async () => undefined)
bunRuntimeMocks.readFile.mockImplementation(
async () => (opts?.readFileValue ?? Buffer.from([1, 2, 3])) as Buffer<ArrayBuffer>,
)
vi.doMock('node:child_process', () => ({ spawnSync }))
vi.doMock('node:fs/promises', () => ({ mkdtemp, rm, writeFile, readFile }))
const http = await import('./http')
return { http, spawnSync, mkdtemp, rm, writeFile, readFile }
return {
http,
spawnSync: bunRuntimeMocks.spawnSync,
mkdtemp: bunRuntimeMocks.mkdtemp,
rm: bunRuntimeMocks.rm,
writeFile: bunRuntimeMocks.writeFile,
readFile: bunRuntimeMocks.readFile,
}
}
describe('http bun runtime', () => {
beforeEach(() => {
vi.resetModules()
vi.clearAllMocks()
vi.unstubAllGlobals()
enableBunRuntime()
Object.defineProperty(process.versions, 'bun', {
value: '1.2.3',
configurable: true,
})
})
afterEach(() => {
restoreBunRuntime()
vi.doUnmock('node:child_process')
vi.doUnmock('node:fs/promises')
vi.unstubAllGlobals()
})
afterAll(() => {
restoreBunRuntime()
})
it('uses curl for apiRequest GET and parses JSON', async () => {
const spawnSync = vi.fn().mockReturnValue({
status: 0,
stdout: '{"ok":true}\n200',
stderr: '',
})
const { http } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
const { http: httpClient } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
const fetchMock = vi.fn()
vi.stubGlobal('fetch', fetchMock)
const result = await http.apiRequest<{ ok: boolean }>('https://registry.example', {
const result = await httpClient.apiRequest<{ ok: boolean }>('https://registry.example', {
method: 'GET',
path: '/v1/ping',
token: 'clh_token',
@@ -98,9 +129,9 @@ describe('http bun runtime', () => {
stdout: '{"ok":true}\n200',
stderr: '',
})
const { http } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
const { http: httpClient } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
await http.apiRequest('https://registry.example', {
await httpClient.apiRequest('https://registry.example', {
method: 'POST',
path: '/v1/ping',
body: { a: 1 },
@@ -118,10 +149,10 @@ describe('http bun runtime', () => {
stdout: 'rate limited\n429',
stderr: '',
})
const { http } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
const { http: httpClient } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
await expect(
http.apiRequest('https://registry.example', {
httpClient.apiRequest('https://registry.example', {
method: 'GET',
path: '/v1/ping',
}),
@@ -138,10 +169,10 @@ describe('http bun runtime', () => {
'rate limited\n__CLAWHUB_CURL_META__\n429\n20\n0\n1771404540\n20\n0\n34\n34\n',
stderr: '',
})
const { http } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
const { http: httpClient } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
await expect(
http.apiRequest('https://registry.example', {
httpClient.apiRequest('https://registry.example', {
method: 'GET',
path: '/v1/ping',
}),
@@ -156,10 +187,10 @@ describe('http bun runtime', () => {
stdout: 'missing\n404',
stderr: '',
})
const { http } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
const { http: httpClient } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
await expect(
http.apiRequest('https://registry.example', {
httpClient.apiRequest('https://registry.example', {
method: 'GET',
path: '/v1/ping',
}),
@@ -181,13 +212,13 @@ describe('http bun runtime', () => {
stdout: '\n400',
stderr: '',
})
const { http } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
const { http: httpClient } = await loadHttpModuleWithBunMocks({ spawnImpl: spawnSync })
const text = await http.fetchText('https://registry.example', { path: '/v1/readme' })
const text = await httpClient.fetchText('https://registry.example', { path: '/v1/readme' })
expect(text).toBe('hello world')
await expect(
http.fetchText('https://registry.example', { path: '/v1/readme' }),
httpClient.fetchText('https://registry.example', { path: '/v1/readme' }),
).rejects.toThrow('HTTP 400')
})
@@ -204,17 +235,17 @@ describe('http bun runtime', () => {
stdout: '404',
stderr: '',
})
const { http, rm, readFile } = await loadHttpModuleWithBunMocks({
const { http: httpClient, rm, readFile } = await loadHttpModuleWithBunMocks({
spawnImpl: spawnSync,
mkdtempValue: '/tmp/clawhub-download-abc',
readFileValue: Buffer.from('not found'),
})
const bytes = await http.downloadZip('https://registry.example', { slug: 'demo', token: 't' })
const bytes = await httpClient.downloadZip('https://registry.example', { slug: 'demo', token: 't' })
expect(Array.from(bytes)).toEqual(Array.from(Buffer.from('not found')))
await expect(
http.downloadZip('https://registry.example', { slug: 'demo', token: 't' }),
httpClient.downloadZip('https://registry.example', { slug: 'demo', token: 't' }),
).rejects.toThrow('not found')
expect(readFile).toHaveBeenCalled()
@@ -230,7 +261,7 @@ describe('http bun runtime', () => {
stdout: '{"ok":true}\n200',
stderr: '',
})
const { http, writeFile, rm } = await loadHttpModuleWithBunMocks({
const { http: httpClient, writeFile, rm } = await loadHttpModuleWithBunMocks({
spawnImpl: spawnSync,
mkdtempValue: '/tmp/clawhub-upload-abc',
})
@@ -239,7 +270,7 @@ describe('http bun runtime', () => {
form.append('name', 'demo')
form.append('file', new Blob(['abc'], { type: 'text/plain' }), 'demo.txt')
const result = await http.apiRequestForm<{ ok: boolean }>('https://registry.example', {
const result = await httpClient.apiRequestForm<{ ok: boolean }>('https://registry.example', {
method: 'POST',
path: '/upload',
form,
+1 -2
View File
@@ -1,5 +1,5 @@
import { spawnSync } from 'node:child_process'
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import pRetry, { AbortError } from 'p-retry'
@@ -587,7 +587,6 @@ function setHeaderIfPresent(headers: Record<string, string>, key: string, value:
async function readFileSafe(path: string) {
try {
const { readFile } = await import('node:fs/promises')
return await readFile(path)
} catch {
return null
+3 -3
View File
@@ -140,12 +140,12 @@ function SoulsIndex() {
className="skills-sort"
value={sort}
onChange={(event) => {
const sort = parseSort(event.target.value)
const nextSort = parseSort(event.target.value)
void navigate({
search: (prev) => ({
...prev,
sort,
dir: parseDir(prev.dir, sort),
sort: nextSort,
dir: parseDir(prev.dir, nextSort),
}),
replace: true,
})
+2 -2
View File
@@ -383,9 +383,9 @@ export function Upload() {
params: isSoulMode ? { slug: trimmedSlug } : { owner: ownerParam, slug: trimmedSlug },
})
}
} catch (error) {
} catch (publishError) {
setStatus(null)
setError(formatPublishError(error))
setError(formatPublishError(publishError))
}
}
+2
View File
@@ -5,6 +5,8 @@ export default defineConfig({
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
testTimeout: 15_000,
hookTimeout: 15_000,
exclude: [
'**/node_modules/**',
'**/dist/**',