mirror of
https://github.com/iflytek/skillhub.git
synced 2026-10-16 04:28:39 +00:00
7 end-to-end tests that exercise the CLI against a real SkillHub backend, auto-skipped when SKILLHUB_E2E_REGISTRY/SKILLHUB_E2E_TOKEN are not set: - E1: publish → search (exact slug) → install → content match → remote remove - E2: publish v1 → publish v2 → install latest gets v2 → --version pin gets v1 - E3: install nonexistent slug → non-zero exit - E4: invalid token whoami → EXIT.auth - E5: keyword substring search finds freshly-published skill - E6: SKILL.md missing name field → server rejects - E7: name exceeding length cap → server rejects Includes shared helpers: live-registry env detection, unique slug generation, polling with timeout, and skill fixture builder.
177 lines
6 KiB
TypeScript
177 lines
6 KiB
TypeScript
/**
|
|
* Track B e2e — full lifecycle against a real SkillHub backend.
|
|
*
|
|
* Covers two scenarios that fake-registry cannot reproduce faithfully:
|
|
* E1 publish → search (exact slug) → install → SKILL.md content matches
|
|
* the published body → remove --remote --hard → search no longer hits.
|
|
* E5 publish → search by keyword (substring of slug) → result set
|
|
* contains the new skill (exercises the search index, not just exact
|
|
* lookup).
|
|
*
|
|
* Skipped automatically when SKILLHUB_E2E_REGISTRY / SKILLHUB_E2E_TOKEN are
|
|
* not set or the backend is unreachable. See helpers/live-registry.ts.
|
|
*/
|
|
import { mkdir, readFile } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import { afterEach, describe, expect, test } from 'bun:test'
|
|
import { createTempHome } from '../helpers/temp-env'
|
|
import { runCli } from '../helpers/run-cli'
|
|
import {
|
|
deleteRemote,
|
|
eventually,
|
|
getLiveRegistry,
|
|
isLiveRegistryReachable,
|
|
uniqueSlug
|
|
} from './helpers/live-registry'
|
|
import { makeSkillDir } from './helpers/skill-fixtures'
|
|
|
|
const live = getLiveRegistry()
|
|
const reachable = live ? await isLiveRegistryReachable(live) : false
|
|
|
|
describe.skipIf(!live || !reachable)('Track B e2e — lifecycle (live backend)', () => {
|
|
// Track every slug that gets published so afterEach can delete them even
|
|
// when the test body throws partway through.
|
|
const publishedSlugs: string[] = []
|
|
|
|
afterEach(async () => {
|
|
while (publishedSlugs.length > 0) {
|
|
const slug = publishedSlugs.pop()!
|
|
await deleteRemote(live!, slug)
|
|
}
|
|
})
|
|
|
|
test('E1 publish → search exact slug → install matches body → remote remove', async () => {
|
|
const env = await createTempHome()
|
|
const slug = uniqueSlug('e2e-trackb-life')
|
|
const fixture = await makeSkillDir(slug)
|
|
|
|
// --- publish -----------------------------------------------------------
|
|
const pubResult = await runCli(
|
|
[
|
|
'publish', fixture.dir,
|
|
'--namespace', 'global',
|
|
'--registry', live!.url,
|
|
'--token', live!.token,
|
|
'--json'
|
|
],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
)
|
|
if (pubResult.exitCode !== 0) {
|
|
// eslint-disable-next-line no-console
|
|
console.error('E1 publish failed', { stdout: pubResult.stdout, stderr: pubResult.stderr })
|
|
}
|
|
expect(pubResult.exitCode).toBe(0)
|
|
const pubJson = JSON.parse(pubResult.stdout) as {
|
|
ok: boolean
|
|
slug: string
|
|
version: string
|
|
namespace: string
|
|
}
|
|
expect(pubJson.ok).toBe(true)
|
|
expect(pubJson.slug).toBe(slug)
|
|
expect(pubJson.namespace).toBe('global')
|
|
publishedSlugs.push(slug)
|
|
|
|
// --- search (exact slug) — allow indexing latency ---------------------
|
|
await eventually(
|
|
() => runCli(
|
|
['search', slug, '--registry', live!.url, '--json'],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
),
|
|
r => r.exitCode === 0 && JSON.parse(r.stdout).items.some(
|
|
(i: { slug: string }) => i.slug === slug
|
|
),
|
|
{ timeoutMs: 8000, intervalMs: 300 }
|
|
)
|
|
|
|
// --- install -----------------------------------------------------------
|
|
const installDir = join(env.cwd, 'live-install')
|
|
await mkdir(installDir, { recursive: true })
|
|
const insResult = await runCli(
|
|
[
|
|
'install', slug,
|
|
'--namespace', 'global',
|
|
'--dir', installDir,
|
|
'--registry', live!.url,
|
|
'--token', live!.token
|
|
],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
)
|
|
expect(insResult.exitCode).toBe(0)
|
|
|
|
// --- content integrity check ------------------------------------------
|
|
const skillMd = await readFile(join(installDir, slug, 'SKILL.md'), 'utf-8')
|
|
expect(skillMd).toContain(`name: ${slug}`)
|
|
expect(skillMd).toContain(fixture.bodyMarker)
|
|
|
|
const meta = JSON.parse(
|
|
await readFile(join(installDir, slug, '.skillhub', 'metadata.json'), 'utf-8')
|
|
) as { registry: string; namespace: string; slug: string; version: string }
|
|
expect(meta.namespace).toBe('global')
|
|
expect(meta.slug).toBe(slug)
|
|
expect(meta.version).toBe(pubJson.version)
|
|
expect(meta.registry).toBe(live!.url)
|
|
|
|
// --- remote remove + post-condition: search no longer hits ------------
|
|
const rmResult = await runCli(
|
|
[
|
|
'remove', slug,
|
|
'--remote', '--hard',
|
|
'--namespace', 'global',
|
|
'--registry', live!.url,
|
|
'--token', live!.token,
|
|
'--json'
|
|
],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
)
|
|
expect(rmResult.exitCode).toBe(0)
|
|
// Mark cleanup-done so afterEach doesn't double-delete.
|
|
publishedSlugs.length = 0
|
|
|
|
await eventually(
|
|
() => runCli(
|
|
['search', slug, '--registry', live!.url, '--json'],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
),
|
|
r => r.exitCode === 0 && JSON.parse(r.stdout).items.every(
|
|
(i: { slug: string }) => i.slug !== slug
|
|
),
|
|
{ timeoutMs: 8000, intervalMs: 300 }
|
|
)
|
|
}, 30_000)
|
|
|
|
test('E5 search by keyword substring finds a freshly-published skill', async () => {
|
|
const env = await createTempHome()
|
|
// Embed a distinctive token in the slug; we'll search for that token
|
|
// alone, not the full slug, so we exercise substring/keyword matching.
|
|
const keyword = `kw${Date.now().toString(36).slice(-6)}`
|
|
const slug = `e2e-trackb-${keyword}-search`
|
|
const fixture = await makeSkillDir(slug, {
|
|
description: `Has the keyword ${keyword} for substring search.`
|
|
})
|
|
|
|
const pubResult = await runCli(
|
|
[
|
|
'publish', fixture.dir,
|
|
'--namespace', 'global',
|
|
'--registry', live!.url,
|
|
'--token', live!.token,
|
|
'--json'
|
|
],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
)
|
|
expect(pubResult.exitCode).toBe(0)
|
|
publishedSlugs.push(slug)
|
|
|
|
await eventually(
|
|
() => runCli(
|
|
['search', keyword, '--registry', live!.url, '--json'],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
),
|
|
r => r.exitCode === 0 && JSON.parse(r.stdout).items.some(
|
|
(i: { slug: string }) => i.slug === slug
|
|
),
|
|
{ timeoutMs: 8000, intervalMs: 300 }
|
|
)
|
|
}, 30_000)
|
|
})
|