test(cli): prove lifecycle warning outputs

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-01 20:33:52 +08:00
parent b9be415d34
commit 8444313fdf
5 changed files with 56 additions and 4 deletions

View file

@ -135,7 +135,7 @@ async function resolveSyncContext(options: SyncCommonOptions): Promise<{
return { client: new SkillHubClient(registry, token), registry, token, namespace, rootDir }
}
function renderPullResult(result: PullResult, json: boolean, check: boolean): string {
export function renderPullResult(result: PullResult, json: boolean, check: boolean): string {
if (json) {
return JSON.stringify({ ok: result.failures.length === 0, check, ...result })
}

View file

@ -96,7 +96,7 @@ function renderUpgradePlan(
].join('\n')
}
function renderUpgradeResult(plan: UpgradePlan, result: UpgradeExecutionResult, json: boolean): string {
export function renderUpgradeResult(plan: UpgradePlan, result: UpgradeExecutionResult, json: boolean): string {
const executionByCoordinate = new Map(result.items.map(item => [item.coordinate, item]))
const items = plan.items.map(item => ({
coordinate: item.coordinate,

View file

@ -64,6 +64,10 @@ export interface UpgradeExecutionResult {
notAttempted: number
}
type UpgradeExecutionOptions = Pick<UpgradeSelectionOptions, 'home' | 'tokenForRegistry'> & {
installSkillFn?: typeof installSkill
}
export async function planSkillUpgrades(options: UpgradeSelectionOptions): Promise<UpgradePlan> {
if (options.coordinates.length === 0) {
throw new CliError('provide at least one installed skill coordinate', EXIT.usage)
@ -223,7 +227,7 @@ export async function planSkillUpgrades(options: UpgradeSelectionOptions): Promi
export async function executeSkillUpgradePlan(
plan: UpgradePlan,
options: Pick<UpgradeSelectionOptions, 'home' | 'tokenForRegistry'>
options: UpgradeExecutionOptions
): Promise<UpgradeExecutionResult> {
if (plan.blocked > 0) {
throw new CliError('upgrade plan contains blocked skills', EXIT.validation)
@ -244,7 +248,7 @@ export async function executeSkillUpgradePlan(
try {
const token = await options.tokenForRegistry(item.registry)
const installed = await installSkill({
const installed = await (options.installSkillFn ?? installSkill)({
registry: item.registry,
token,
namespace: item.inventoryItem.namespace,

View file

@ -8,6 +8,7 @@ import { runCli } from '../helpers/run-cli'
import { createTempHome } from '../helpers/temp-env'
import { SkillHubClient } from '../../src/clients/skillhub-client'
import { pullNamespace } from '../../src/services/sync-service'
import { renderPullResult } from '../../src/commands/sync'
function makeSkill(body: string): { zipBytes: Uint8Array; fingerprint: string } {
const content = strToU8(body)
@ -47,6 +48,8 @@ describe('sync command', () => {
slug: 'demo',
message: 'target lock cleanup failed: simulated release failure'
}])
expect(JSON.parse(renderPullResult(result, true, false)).warnings).toEqual(result.warnings)
expect(renderPullResult(result, false, false)).toContain('warning demo: target lock cleanup failed')
} finally {
registry.stop()
}

View file

@ -6,6 +6,8 @@ import { createTempHome } from '../helpers/temp-env'
import { startFakeRegistry } from '../helpers/fake-registry'
import { runCli } from '../helpers/run-cli'
import { executeSkillUpgradePlan, planSkillUpgrades } from '../../src/services/upgrade-service'
import { installSkill } from '../../src/services/install-service'
import { renderUpgradeResult } from '../../src/commands/upgrade'
let registries: Array<Awaited<ReturnType<typeof startFakeRegistry>>> = []
@ -575,6 +577,49 @@ describe('upgrade command', () => {
expect(registry.received.downloads).toBe(5)
})
test('a committed upgrade keeps success and renders a post-commit warning', async () => {
const env = await createTempHome()
const skill = { namespace: 'global', slug: 'warned', version: '1.0.0', fingerprint: 'v1', zipBytes: makeSkillZip('# v1') }
const registry = await startFakeRegistry({ skills: [skill] })
registries.push(registry)
const rootDir = join(env.cwd, 'skills')
await mkdir(rootDir, { recursive: true })
await runCli(['install', '@global/warned', '--dir', rootDir, '--registry', registry.url], {
HOME: env.home,
USERPROFILE: env.home
})
skill.version = '1.1.0'
skill.fingerprint = 'v2'
skill.zipBytes = makeSkillZip('# v2')
const tokenForRegistry = async () => undefined
const plan = await planSkillUpgrades({
coordinates: ['@global/warned'],
registry: registry.url,
force: false,
home: env.home,
tokenForRegistry
})
const result = await executeSkillUpgradePlan(plan, {
home: env.home,
tokenForRegistry,
installSkillFn: options => installSkill({
...options,
acquireTargetLock: async () => async () => { throw new Error('simulated release failure') }
})
})
expect(result).toMatchObject({ upgraded: 1, failed: 0 })
expect(result.items[0]).toMatchObject({ action: 'upgraded' })
expect(result.items[0]?.warnings).toEqual(['target lock cleanup failed: simulated release failure'])
expect(JSON.parse(renderUpgradeResult(plan, result, true)).items[0].warnings).toHaveLength(1)
expect(renderUpgradeResult(plan, result, false)).toContain('upgraded')
expect(renderUpgradeResult(plan, result, false)).toContain('[warning: target lock cleanup failed')
expect(await readFile(join(rootDir, 'warned', 'SKILL.md'), 'utf-8')).toBe('# v2')
const inventory = JSON.parse(await readFile(join(env.home, '.skillhub', 'inventory.json'), 'utf-8'))
expect(inventory.items[0]).toMatchObject({ version: '1.1.0', fingerprint: 'v2' })
})
test('legacy metadata without a file baseline requires explicit force migration', async () => {
const env = await createTempHome()
const skill = { namespace: 'global', slug: 'legacy', version: '1.0.0', fingerprint: 'v1', zipBytes: makeSkillZip('# v1') }