mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-28 11:25:00 +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.
144 lines
5.6 KiB
TypeScript
144 lines
5.6 KiB
TypeScript
/**
|
|
* Track B e2e — real-backend error handling.
|
|
*
|
|
* E3 install <nonexistent slug> against the real backend exits non-zero
|
|
* and surfaces a meaningful error mentioning the slug or 4xx status.
|
|
* This is the regression line for the "registry returned 400" UX
|
|
* observed during manual testing.
|
|
* E4 whoami with an invalid bearer token exits with EXIT.auth (2) and
|
|
* contains an auth-related token in stderr — confirming the real
|
|
* backend speaks the expected 401 / unauthorized contract.
|
|
*/
|
|
import { mkdir } from 'node:fs/promises'
|
|
import { join } from 'node:path'
|
|
import { describe, expect, test } from 'bun:test'
|
|
import { createTempHome } from '../helpers/temp-env'
|
|
import { runCli } from '../helpers/run-cli'
|
|
import { getLiveRegistry, isLiveRegistryReachable, uniqueSlug } from './helpers/live-registry'
|
|
|
|
const live = getLiveRegistry()
|
|
const reachable = live ? await isLiveRegistryReachable(live) : false
|
|
|
|
describe.skipIf(!live || !reachable)('Track B e2e — backend errors (live backend)', () => {
|
|
test('E3 install of a nonexistent slug exits non-zero with a meaningful message', async () => {
|
|
const env = await createTempHome()
|
|
const slug = uniqueSlug('e2e-trackb-missing')
|
|
|
|
const installDir = join(env.cwd, 'should-not-be-populated')
|
|
await mkdir(installDir, { recursive: true })
|
|
|
|
const result = await runCli(
|
|
[
|
|
'install', slug,
|
|
'--namespace', 'global',
|
|
'--dir', installDir,
|
|
'--registry', live!.url,
|
|
'--token', live!.token
|
|
],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
)
|
|
|
|
expect(result.exitCode).not.toBe(0)
|
|
// Either a 4xx status or a "not found" string should appear; the exact
|
|
// wording depends on how the CLI maps server response codes today.
|
|
const stderrLower = result.stderr.toLowerCase()
|
|
expect(
|
|
/400|404|not found/.test(stderrLower)
|
|
).toBe(true)
|
|
|
|
// No metadata file should have been created.
|
|
const metaPath = join(installDir, slug, '.skillhub', 'metadata.json')
|
|
expect(await Bun.file(metaPath).exists()).toBe(false)
|
|
}, 15_000)
|
|
|
|
test('E4 whoami with an invalid token exits with EXIT.auth and reports auth failure', async () => {
|
|
const env = await createTempHome()
|
|
|
|
const result = await runCli(
|
|
[
|
|
'whoami',
|
|
'--registry', live!.url,
|
|
'--token', 'sk_definitely_not_a_real_token_xxxxxxxxxx'
|
|
],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
)
|
|
|
|
expect(result.exitCode).toBe(2) // EXIT.auth
|
|
expect(result.stderr.toLowerCase()).toMatch(/auth|unauthorized|401/)
|
|
}, 15_000)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// E6 — Real backend rejects a malformed SKILL.md (no `name` frontmatter
|
|
// field). The CLI must propagate the rejection without crashing or
|
|
// leaving partial state on the server.
|
|
// ---------------------------------------------------------------------------
|
|
test('E6 publish a SKILL.md missing the required `name` field is rejected by the real backend', async () => {
|
|
const { mkdtemp, writeFile } = await import('node:fs/promises')
|
|
const { join } = await import('node:path')
|
|
const { tmpdir } = await import('node:os')
|
|
const env = await createTempHome()
|
|
|
|
const dir = await mkdtemp(join(tmpdir(), 'skillhub-e2e-bad-name-'))
|
|
// Frontmatter has description but no name — server-side parser must
|
|
// reject this as a missing required field.
|
|
await writeFile(join(dir, 'SKILL.md'), [
|
|
'---',
|
|
'description: missing name field on purpose',
|
|
'---',
|
|
'',
|
|
'# Body'
|
|
].join('\n'))
|
|
|
|
const result = await runCli(
|
|
[
|
|
'publish', dir,
|
|
'--namespace', 'global',
|
|
'--registry', live!.url,
|
|
'--token', live!.token
|
|
],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
)
|
|
expect(result.exitCode).not.toBe(0)
|
|
// The error should mention either the validation reason or a 4xx
|
|
// status code so the user has a starting point for debugging.
|
|
expect(result.stderr.length).toBeGreaterThan(0)
|
|
expect(result.stderr.length).toBeLessThan(2000)
|
|
}, 15_000)
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// E7 — Real backend enforces the 64-char limit on token-style names.
|
|
// Skill `name` is the slug; servers in this project enforce a length
|
|
// cap. We assert that overlong names are surfaced as a non-zero exit.
|
|
// ---------------------------------------------------------------------------
|
|
test('E7 publish with a name far exceeding the server length cap is rejected', async () => {
|
|
const { mkdtemp, writeFile } = await import('node:fs/promises')
|
|
const { join } = await import('node:path')
|
|
const { tmpdir } = await import('node:os')
|
|
const env = await createTempHome()
|
|
|
|
const overlongName = `${'overlong-name-'.repeat(20)}-end` // ~280 chars
|
|
const dir = await mkdtemp(join(tmpdir(), 'skillhub-e2e-overlong-name-'))
|
|
await writeFile(join(dir, 'SKILL.md'), [
|
|
'---',
|
|
`name: ${overlongName}`,
|
|
'description: name is way past any reasonable cap',
|
|
'---',
|
|
'',
|
|
'# Body'
|
|
].join('\n'))
|
|
|
|
const result = await runCli(
|
|
[
|
|
'publish', dir,
|
|
'--namespace', 'global',
|
|
'--registry', live!.url,
|
|
'--token', live!.token
|
|
],
|
|
{ HOME: env.home, USERPROFILE: env.home }
|
|
)
|
|
// We don't assert the exact 64-char limit (it's a server-side detail
|
|
// that may change). We just assert "real backend rejected this".
|
|
expect(result.exitCode).not.toBe(0)
|
|
expect(result.stderr.length).toBeGreaterThan(0)
|
|
}, 15_000)
|
|
})
|