From 069a30fce63ca245d65a09b18ecf6a493dbb1537 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Tue, 1 Sep 2026 18:57:37 +0800 Subject: [PATCH] fix(cli): keep target lock identity stable Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- cli/src/services/skill-target-lock.ts | 2 +- .../unit/services/skill-target-lock.test.ts | 21 ++++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/cli/src/services/skill-target-lock.ts b/cli/src/services/skill-target-lock.ts index ab93ac4b..cd32cfd1 100644 --- a/cli/src/services/skill-target-lock.ts +++ b/cli/src/services/skill-target-lock.ts @@ -28,7 +28,7 @@ export async function acquireSkillTargetLock(rootDir: string, slug: string): Pro export async function skillTargetLockPath(rootDir: string, slug: string): Promise { 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}`) diff --git a/cli/test/unit/services/skill-target-lock.test.ts b/cli/test/unit/services/skill-target-lock.test.ts index 444d8f56..a8c97017 100644 --- a/cli/test/unit/services/skill-target-lock.test.ts +++ b/cli/test/unit/services/skill-target-lock.test.ts @@ -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) + }) })