skillhub/cli/test/integration/version-upgrade-flow.test.ts
dongmucat 9643e4157c test(cli): migrate comprehensive test suite from test/cli-integration-coverage
Migrated 39 test files covering CLI integration and unit testing:
- 6 new integration tests (auth-resolution, concurrency, cross-command, inventory-resilience, multi-registry, version-upgrade-flow)
- Enhanced 7 existing integration tests with comprehensive scenarios
- Updated 2 unit tests with correct exit code expectations

All tests use fake registry approach (no E2E/browser required) and pass lint/build/test checks.
2026-05-13 11:14:20 +08:00

162 lines
6.5 KiB
TypeScript

/**
* CLI skill version-upgrade flow.
*
* These tests cover scenarios that span multiple `install` invocations
* against a registry whose state changes between runs — i.e. the user-facing
* "upgrade an installed skill" workflow. They complement the per-command
* tests in install-command.test.ts which use a single static registry.
*
* Coverage focus (per test-case-design-skill methodology):
* - VU1 (state-transition): full v1 → v2 upgrade lifecycle. Asserts that
* metadata.json, inventory.json AND the on-disk bundle all reflect v2
* after `install --force`, with no orphaned v1 entry left behind.
* - VU2 (equivalence-class on `--version`): pinning to a specific version
* forwards `?version=` to /resolve so the registry can serve the
* intended bundle.
*/
import { mkdir, readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { afterEach, describe, expect, test } from 'bun:test'
import { zipSync, strToU8 } from 'fflate'
import { createTempHome } from '../helpers/temp-env'
import { startFakeRegistry } from '../helpers/fake-registry'
import { runCli } from '../helpers/run-cli'
function makeSkillZipWithBody(body: string): Uint8Array {
return zipSync({ 'SKILL.md': strToU8(body) })
}
let registry: Awaited<ReturnType<typeof startFakeRegistry>> | undefined
afterEach(() => {
registry?.stop()
registry = undefined
})
describe('version upgrade flow', () => {
// -------------------------------------------------------------------------
// VU1 — full upgrade lifecycle:
// 1. Registry serves pdf-parser@1.0.0 → install → metadata=v1, content=v1
// 2. Stop registry, start a new one serving pdf-parser@2.0.0
// 3. Install --force using the new registry URL
// 4. metadata.json, inventory.json AND on-disk SKILL.md all reflect v2
// -------------------------------------------------------------------------
test('VU1 install v1 then upgrade to v2 with --force replaces metadata, inventory, and content', async () => {
const env = await createTempHome()
// --- Stage 1: install v1 ----------------------------------------------
registry = await startFakeRegistry({
token: 'sk_ok',
user: { handle: 'u1', displayName: 'User One' },
skills: [{
namespace: 'global',
slug: 'pdf-parser',
version: '1.0.0',
zipBytes: makeSkillZipWithBody('# pdf-parser v1\n\nVersion one body.')
}]
})
await runCli(['login', '--registry', registry.url, '--token', 'sk_ok'], { HOME: env.home, USERPROFILE: env.home })
const installDir = join(env.cwd, 'skills-upgrade')
await mkdir(installDir, { recursive: true })
const r1 = await runCli(
['install', 'pdf-parser', '--dir', installDir, '--registry', registry.url, '--token', 'sk_ok'],
{ HOME: env.home, USERPROFILE: env.home }
)
expect(r1.exitCode).toBe(0)
const skillFile = join(installDir, 'pdf-parser', 'SKILL.md')
const metaPath = join(installDir, 'pdf-parser', '.skillhub', 'metadata.json')
const inventoryPath = join(env.home, '.skillhub', 'inventory.json')
{
const meta = JSON.parse(await readFile(metaPath, 'utf-8'))
expect(meta.version).toBe('1.0.0')
const body = await readFile(skillFile, 'utf-8')
expect(body).toContain('Version one body.')
}
// --- Stage 2: swap registry to v2 -------------------------------------
registry.stop()
registry = await startFakeRegistry({
token: 'sk_ok',
user: { handle: 'u1', displayName: 'User One' },
skills: [{
namespace: 'global',
slug: 'pdf-parser',
version: '2.0.0',
zipBytes: makeSkillZipWithBody('# pdf-parser v2\n\nVersion two body.')
}]
})
// Re-login against the new registry (URL changed, so credentials are
// keyed differently).
await runCli(['login', '--registry', registry.url, '--token', 'sk_ok'], { HOME: env.home, USERPROFILE: env.home })
const r2 = await runCli(
['install', 'pdf-parser', '--dir', installDir, '--registry', registry.url, '--token', 'sk_ok', '--force'],
{ HOME: env.home, USERPROFILE: env.home }
)
expect(r2.exitCode).toBe(0)
// --- Stage 3: assert end state ----------------------------------------
const finalMeta = JSON.parse(await readFile(metaPath, 'utf-8'))
expect(finalMeta.version).toBe('2.0.0')
expect(finalMeta.registry).toBe(registry.url)
const finalBody = await readFile(skillFile, 'utf-8')
expect(finalBody).toContain('Version two body.')
expect(finalBody).not.toContain('Version one body.')
const inventory = JSON.parse(await readFile(inventoryPath, 'utf-8')) as {
items: Array<{ namespace: string; slug: string; version: string; targets: Array<{ installDir: string }> }>
}
const matching = inventory.items.filter(i => i.namespace === 'global' && i.slug === 'pdf-parser')
expect(matching).toHaveLength(1) // no duplicate v1 entry
expect(matching[0]?.version).toBe('2.0.0')
})
// -------------------------------------------------------------------------
// VU2 — version pinning:
// `install --version=X` must forward ?version=X to /resolve so the
// registry can serve the requested bundle. The fake registry captures
// the resolve query for assertion.
// -------------------------------------------------------------------------
test('VU2 --version pins resolve to the requested version string', async () => {
const env = await createTempHome()
registry = await startFakeRegistry({
token: 'sk_ok',
user: { handle: 'u1', displayName: 'User One' },
skills: [{
namespace: 'global',
slug: 'pdf-parser',
version: '1.5.0',
zipBytes: makeSkillZipWithBody('# pinned')
}]
})
await runCli(['login', '--registry', registry.url, '--token', 'sk_ok'], { HOME: env.home, USERPROFILE: env.home })
const installDir = join(env.cwd, 'skills-pin')
await mkdir(installDir, { recursive: true })
const result = await runCli(
[
'install', 'pdf-parser',
'--version', '1.5.0',
'--dir', installDir,
'--registry', registry.url,
'--token', 'sk_ok'
],
{ HOME: env.home, USERPROFILE: env.home }
)
expect(result.exitCode).toBe(0)
expect(registry.received.resolve?.namespace).toBe('global')
expect(registry.received.resolve?.slug).toBe('pdf-parser')
expect(registry.received.resolve?.version).toBe('1.5.0')
const meta = JSON.parse(await readFile(join(installDir, 'pdf-parser', '.skillhub', 'metadata.json'), 'utf-8'))
expect(meta.version).toBe('1.5.0')
})
})