mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
test: fix lint and coverage compatibility after upgrades
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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/**',
|
||||
|
||||
Reference in New Issue
Block a user