diff --git a/src/main/runtime/runtime-rpc-mobile-method-allowlist.test.ts b/src/main/runtime/runtime-rpc-mobile-method-allowlist.test.ts index b0d5b18b84c..9c590994c41 100644 --- a/src/main/runtime/runtime-rpc-mobile-method-allowlist.test.ts +++ b/src/main/runtime/runtime-rpc-mobile-method-allowlist.test.ts @@ -479,7 +479,6 @@ describe('OrcaRuntimeRpcServer', () => { deviceToken: mobile.token, params: { worktree: 'id:wt-1', page: 'page-1' } }) - expect(replies).toContainEqual( expect.objectContaining({ id: 'req_forbidden', diff --git a/src/main/skills/skill-install-lock-release.test.ts b/src/main/skills/skill-install-lock-release.test.ts new file mode 100644 index 00000000000..6811e0d08f9 --- /dev/null +++ b/src/main/skills/skill-install-lock-release.test.ts @@ -0,0 +1,36 @@ +import { mkdtemp, mkdir, readdir, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, describe, expect, it } from 'vitest' +import { cleanupReleasedSkillInstallLock } from './skill-install-lock-release' + +const roots: string[] = [] + +afterEach(async () => { + await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true }))) +}) + +describe('cleanupReleasedSkillInstallLock', () => { + it('keeps a released lock recoverable when directory removal races', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-skill-lock-release-test-')) + roots.push(root) + const releasePath = join(root, 'released') + const token = '11111111-1111-4111-8111-111111111111' + await mkdir(releasePath) + await Promise.all([ + writeFile(join(releasePath, `${token}.owner`), 'owner'), + writeFile(join(releasePath, `${token}.released`), '') + ]) + + const removeDirectory = async (): Promise => { + const error = new Error('injected-rmdir-race') as NodeJS.ErrnoException + error.code = 'ENOTEMPTY' + throw error + } + + await expect( + cleanupReleasedSkillInstallLock(releasePath, token, removeDirectory) + ).resolves.toBeUndefined() + await expect(readdir(releasePath)).resolves.toEqual([]) + }) +}) diff --git a/src/main/skills/skill-install-lock-release.ts b/src/main/skills/skill-install-lock-release.ts index 0b56ffcc9a1..b3e02bf869b 100644 --- a/src/main/skills/skill-install-lock-release.ts +++ b/src/main/skills/skill-install-lock-release.ts @@ -2,7 +2,7 @@ import { readdir, rmdir, unlink } from 'node:fs/promises' import { join } from 'node:path' const RELEASE_ENTRY_NAME = /^[a-f0-9-]{36}\.(?:owner|released)$/ -const RECOVERY_RMDIR_IGNORED_CODES = new Set(['ENOENT', 'ENOTEMPTY', 'EEXIST', 'EBUSY']) +const LOCK_DIRECTORY_RMDIR_IGNORED_CODES = new Set(['ENOENT', 'ENOTEMPTY', 'EEXIST', 'EBUSY']) async function unlinkIfPresent(path: string): Promise { await unlink(path).catch((error) => { @@ -17,7 +17,7 @@ async function removeDirectoryIfPresent( removeDirectory: (path: string) => Promise ): Promise { await removeDirectory(path).catch((error) => { - if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { + if (!LOCK_DIRECTORY_RMDIR_IGNORED_CODES.has((error as NodeJS.ErrnoException).code ?? '')) { throw error } }) @@ -46,7 +46,7 @@ export async function reclaimReleasedSkillInstallLock(path: string): Promise unlinkIfPresent(join(path, entry.name))) ) await rmdir(path).catch((error) => { - if (!RECOVERY_RMDIR_IGNORED_CODES.has((error as NodeJS.ErrnoException).code ?? '')) { + if (!LOCK_DIRECTORY_RMDIR_IGNORED_CODES.has((error as NodeJS.ErrnoException).code ?? '')) { throw error } })