mirror of
https://github.com/iflytek/skillhub.git
synced 2026-09-09 22:31:14 +00:00
* feat(cli): add source-safe skill upgrades Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): harden skill upgrade lifecycle Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): make multi-target upgrades failure-safe Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): cover upgrade selection and fallback boundaries Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): use a dead pid for stale lock recovery Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): prove upgrade safety invariants Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): prove manual ownership remains untouched Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): bind ownership sentinels to each fixture Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): keep upgrade assertions registry-scoped Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): close upgrade commit races Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): keep rollback backup path narrowed Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): reject upgrade targets removed after planning Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): serialize remove with target upgrades Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): prove shared target lock cleanup Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): make stale target lock recovery ownership-safe Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): use proven cross-process target locks Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): synchronize target lock contenders Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): bound target lock worker cleanup Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): satisfy target lock worker lint Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): serialize inventory and alias target mutations Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): cover lock root safety boundaries Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): keep target lock identity stable Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): report partial upgrade failures Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): preserve committed upgrade results Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): surface install lifecycle warnings Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): prove lifecycle warning outputs Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): assert structured upgrade failures Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): keep portable install paths Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * fix(cli): unify aliased target identity Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): accept canonical relative paths Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): verify portable target identity Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> * test(cli): synchronize stale lock contenders Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --------- Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
import { describe, expect, test } from 'bun:test'
|
|
import { mkdir, utimes } from 'node:fs/promises'
|
|
import { mkdtemp } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join, dirname } from 'node:path'
|
|
import { InventoryStore } from '../../../src/stores/inventory-store'
|
|
|
|
function makeTempHome() {
|
|
return mkdtemp(join(tmpdir(), 'skillhub-store-test-'))
|
|
}
|
|
|
|
function makeTarget(slug: string) {
|
|
return {
|
|
agent: 'claude',
|
|
rootDir: `/projects/${slug}`,
|
|
installDir: `/projects/${slug}/.skillhub/skills/${slug}`,
|
|
installedAt: new Date().toISOString(),
|
|
}
|
|
}
|
|
|
|
describe('InventoryStore', () => {
|
|
test('5 sequential upsertTarget calls all persist', async () => {
|
|
const home = await makeTempHome()
|
|
const store = new InventoryStore(home)
|
|
|
|
for (let i = 0; i < 5; i++) {
|
|
await store.upsertTarget(
|
|
'https://skill.xfyun.cn',
|
|
'global',
|
|
`skill-${i}`,
|
|
'1.0.0',
|
|
makeTarget(`skill-${i}`),
|
|
)
|
|
}
|
|
|
|
const inventory = await store.read()
|
|
expect(inventory.items).toHaveLength(5)
|
|
})
|
|
|
|
test('recovers from a stale lock directory', async () => {
|
|
const home = await makeTempHome()
|
|
const store = new InventoryStore(home)
|
|
|
|
// proper-lockfile uses atomic mkdir and an mtime lease.
|
|
await mkdir(dirname(store.path), { recursive: true })
|
|
const lockPath = `${store.path}.lock`
|
|
await mkdir(lockPath)
|
|
const staleTime = new Date(Date.now() - 60_000)
|
|
await utimes(lockPath, staleTime, staleTime)
|
|
|
|
// upsertTarget should recover from the stale lock and succeed
|
|
await store.upsertTarget(
|
|
'https://skill.xfyun.cn',
|
|
'global',
|
|
'test-skill',
|
|
'1.0.0',
|
|
makeTarget('test-skill'),
|
|
)
|
|
|
|
const inventory = await store.read()
|
|
expect(inventory.items).toHaveLength(1)
|
|
expect(inventory.items[0]?.slug).toBe('test-skill')
|
|
})
|
|
|
|
test('removeTarget returns false when item not found', async () => {
|
|
const home = await makeTempHome()
|
|
const store = new InventoryStore(home)
|
|
|
|
const result = await store.removeTarget(
|
|
'https://skill.xfyun.cn',
|
|
'global',
|
|
'nonexistent',
|
|
'/some/path',
|
|
)
|
|
expect(result).toBe(false)
|
|
})
|
|
|
|
test('removeTargetsByInstallDir returns 0 when no matches', async () => {
|
|
const home = await makeTempHome()
|
|
const store = new InventoryStore(home)
|
|
|
|
// Seed one item so the inventory file exists
|
|
await store.upsertTarget(
|
|
'https://skill.xfyun.cn',
|
|
'global',
|
|
'existing-skill',
|
|
'1.0.0',
|
|
makeTarget('existing-skill'),
|
|
)
|
|
|
|
const removed = await store.removeTargetsByInstallDir('/nonexistent/path')
|
|
expect(removed).toBe(0)
|
|
|
|
// Original item should still be there
|
|
const inventory = await store.read()
|
|
expect(inventory.items).toHaveLength(1)
|
|
})
|
|
|
|
test('rejects a version change while an unselected target is retained', async () => {
|
|
const home = await makeTempHome()
|
|
const store = new InventoryStore(home)
|
|
const retained = makeTarget('shared-a')
|
|
await store.upsertTarget(
|
|
'https://skill.xfyun.cn',
|
|
'global',
|
|
'shared',
|
|
'1.0.0',
|
|
retained,
|
|
'fp-v1',
|
|
)
|
|
|
|
const replacement = makeTarget('shared-b')
|
|
await expect(store.replaceTargetsAtInstallDirs(
|
|
'https://skill.xfyun.cn',
|
|
'global',
|
|
'shared',
|
|
'1.1.0',
|
|
[replacement],
|
|
'fp-v2',
|
|
)).rejects.toThrow('partial-target install would create inconsistent versions')
|
|
|
|
const inventory = await store.read()
|
|
expect(inventory.items).toHaveLength(1)
|
|
expect(inventory.items[0]).toMatchObject({ version: '1.0.0', fingerprint: 'fp-v1' })
|
|
expect(inventory.items[0]?.targets).toEqual([retained])
|
|
})
|
|
})
|