fix(cli): keep target lock identity stable

Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com>
This commit is contained in:
XiaoSeS 2026-09-01 18:57:37 +08:00
parent 30b1c9e74c
commit 069a30fce6
2 changed files with 21 additions and 2 deletions

View file

@ -28,7 +28,7 @@ export async function acquireSkillTargetLock(rootDir: string, slug: string): Pro
export async function skillTargetLockPath(rootDir: string, slug: string): Promise<string> {
const canonicalRoot = await canonicalizeExistingPath(resolve(rootDir))
const target = await canonicalizeExistingPath(resolve(canonicalRoot, slug))
const target = resolve(canonicalRoot, slug)
const digest = createHash('sha256').update(target).digest('hex')
const uid = typeof process.getuid === 'function' ? process.getuid() : 'user'
const lockDir = join(tmpdir(), `skillhub-cli-target-locks-${uid}`)

View file

@ -1,9 +1,10 @@
import { access, chmod, lstat, mkdir, mkdtemp, symlink, utimes, writeFile } from 'node:fs/promises'
import { access, chmod, lstat, mkdir, mkdtemp, rm, symlink, utimes, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { describe, expect, test } from 'bun:test'
import {
acquireSkillTargetLock,
assertPrivateLockDir,
ensurePrivateLockDir,
skillTargetLockPath
@ -108,4 +109,22 @@ describe('skill target lifecycle lock', () => {
expect((await lstat(dirname(lockPath))).mode & 0o077).toBe(0)
}
})
test('keeps one lock identity when a symlink target is removed', async () => {
const rootDir = await mkdtemp(join(tmpdir(), 'skillhub-target-symlink-root-'))
const linkedDir = await mkdtemp(join(tmpdir(), 'skillhub-target-symlink-value-'))
const skillDir = join(rootDir, 'demo')
await symlink(linkedDir, skillDir, 'dir')
const lockPathBeforeRemoval = await skillTargetLockPath(rootDir, 'demo')
const release = await acquireSkillTargetLock(rootDir, 'demo')
try {
await rm(skillDir)
expect(await skillTargetLockPath(rootDir, 'demo')).toBe(lockPathBeforeRemoval)
await expect(acquireSkillTargetLock(rootDir, 'demo')).rejects.toThrow('install target is busy')
} finally {
await release()
}
expect(await exists(lockPathBeforeRemoval)).toBe(false)
})
})