skillhub/cli/test/integration/inventory-resilience.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

107 lines
4.4 KiB
TypeScript

/**
* inventory.json resilience.
*
* inventory.json is the local manifest of installed skills. These tests pin
* how the CLI behaves when that file is corrupt or written by overlapping
* operations:
* - list against a corrupt inventory should fail loudly (not silently)
* - install against a corrupt inventory should still complete the
* filesystem extraction even if inventory bookkeeping fails — partial
* state surfaces a clear error
* - sequential installs of distinct skills do not corrupt the manifest
*
* The unit test in test/unit/stores/inventory-store.test.ts asserts the
* lock-file recovery path. These cover the user-facing CLI surface.
*/
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { join } from 'node:path'
import { afterEach, describe, expect, test } from 'bun:test'
import { zipSync, strToU8 } from 'fflate'
import { startFakeRegistry } from '../helpers/fake-registry'
import { runCli } from '../helpers/run-cli'
import { createTempHome } from '../helpers/temp-env'
let registry: Awaited<ReturnType<typeof startFakeRegistry>> | undefined
afterEach(() => {
registry?.stop(); registry = undefined
})
function makeSkillZip(): Uint8Array {
return zipSync({ 'SKILL.md': strToU8('# test') })
}
describe('inventory resilience', () => {
test('list exits non-zero when inventory.json is malformed (documents current generic-error UX)', async () => {
const env = await createTempHome()
await mkdir(join(env.home, '.skillhub'), { recursive: true })
await writeFile(join(env.home, '.skillhub', 'inventory.json'), '{ this is not JSON')
const result = await runCli(['list'], { HOME: env.home, USERPROFILE: env.home })
// Contract: CLI must not crash silently or print a stack trace. It
// exits non-zero and emits a short message.
expect(result.exitCode).not.toBe(0)
expect(result.stderr.length).toBeGreaterThan(0)
expect(result.stderr.length).toBeLessThan(2000)
// Documented gap: today's message is the generic "unexpected failure"
// and does not mention `inventory` or `JSON`. When the CLI surfaces a
// more specific message in the future, tighten this assertion.
expect(result.stderr).toContain('Error')
})
test('list --json on a corrupt inventory emits a parseable error envelope (not a stack trace)', async () => {
const env = await createTempHome()
await mkdir(join(env.home, '.skillhub'), { recursive: true })
await writeFile(join(env.home, '.skillhub', 'inventory.json'), '{"items":')
const result = await runCli(['list', '--json'], { HOME: env.home, USERPROFILE: env.home })
expect(result.exitCode).not.toBe(0)
const candidate = result.stdout || result.stderr
expect(candidate.length).toBeLessThan(2000)
// Contract: --json error path is machine-parseable, regardless of the
// (currently generic) human message.
const json = JSON.parse(candidate) as { ok: boolean; message: string; exitCode: number }
expect(json.ok).toBe(false)
expect(typeof json.message).toBe('string')
expect(json.exitCode).toBe(result.exitCode)
})
test('two sequential installs of distinct slugs leave a coherent inventory', async () => {
const env = await createTempHome()
registry = await startFakeRegistry({
token: 'sk_ok',
user: { handle: 'u', displayName: 'U' },
skills: [
{ namespace: 'global', slug: 'one', version: '1.0.0', zipBytes: makeSkillZip() },
{ namespace: 'global', slug: 'two', version: '1.0.0', zipBytes: makeSkillZip() }
]
})
const baseDir = join(env.cwd, 'pool')
await mkdir(baseDir, { recursive: true })
const r1 = await runCli(
['install', 'one', '--dir', baseDir, '--registry', registry.url, '--token', 'sk_ok'],
{ HOME: env.home, USERPROFILE: env.home }
)
expect(r1.exitCode).toBe(0)
const r2 = await runCli(
['install', 'two', '--dir', baseDir, '--registry', registry.url, '--token', 'sk_ok'],
{ HOME: env.home, USERPROFILE: env.home }
)
expect(r2.exitCode).toBe(0)
const inventory = JSON.parse(
await readFile(join(env.home, '.skillhub', 'inventory.json'), 'utf-8')
) as { items: Array<{ slug: string; targets: Array<{ installDir: string }> }> }
const slugs = inventory.items.map(i => i.slug).sort()
expect(slugs).toEqual(['one', 'two'])
for (const item of inventory.items) {
expect(item.targets.length).toBeGreaterThan(0)
}
})
})