From 7d6d7ca6f841acf686a8dd07c1dbcc7cf40073fa Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Wed, 7 Oct 2026 09:20:57 -0700 Subject: [PATCH] fix(skills): observe archive abort errors before reading (#26246) --- src/main/skills/skill-package-tar.test.ts | 33 ++++++++++++++++++++++- src/main/skills/skill-package-tar.ts | 2 ++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/src/main/skills/skill-package-tar.test.ts b/src/main/skills/skill-package-tar.test.ts index 4354c5d0443..5ed98a39d54 100644 --- a/src/main/skills/skill-package-tar.test.ts +++ b/src/main/skills/skill-package-tar.test.ts @@ -1,5 +1,13 @@ import { describe, expect, it } from 'vitest' -import { parseSkillTarHeader, SKILL_TAR_BLOCK_BYTES } from './skill-package-tar' +import { mkdtemp, rm } from 'node:fs/promises' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { + openSkillTarGzip, + parseSkillTarHeader, + SKILL_TAR_BLOCK_BYTES, + writeSkillTarGzip +} from './skill-package-tar' function writeOctal(header: Buffer, offset: number, length: number, value: number): void { header.write(`${value.toString(8).padStart(length - 1, '0')}\0`, offset, length, 'ascii') @@ -62,3 +70,26 @@ describe('skill package tar envelope', () => { } }) }) + +describe('skill tar reader error ownership', () => { + it('observes an abort after the pipeline completes before the first reader request', async () => { + const root = await mkdtemp(join(tmpdir(), 'orca-skill-tar-abort-')) + try { + const archivePath = join(root, 'archive.tar.gz') + await writeSkillTarGzip(archivePath, [ + { path: 'SKILL.md', size: 1, executable: false, bytes: Buffer.from('x') } + ]) + const archive = await openSkillTarGzip(archivePath) + await archive.archiveIdentity + const failure = Object.assign(new Error('staging directory already exists'), { + code: 'EEXIST' + }) + archive.abort(failure) + // Observe destruction before lazily attaching the reader's error listener. + await new Promise((resolve) => setImmediate(resolve)) + await expect(archive.reader.readExact(SKILL_TAR_BLOCK_BYTES)).rejects.toBe(failure) + } finally { + await rm(root, { recursive: true, force: true }) + } + }) +}) diff --git a/src/main/skills/skill-package-tar.ts b/src/main/skills/skill-package-tar.ts index f8cac20c633..4a67f7ca553 100644 --- a/src/main/skills/skill-package-tar.ts +++ b/src/main/skills/skill-package-tar.ts @@ -262,6 +262,8 @@ export async function openSkillTarGzip(archivePath: string): Promise<{ } }) const gunzip = createGunzip() + // Observe late aborts before the archive reader starts. + gunzip.on('error', () => undefined) const completion = pipeline(source, verifier, gunzip) const archiveIdentity = completion.then(() => ({ archiveSha256: archiveHash.digest('hex'),