diff --git a/apps/code/src/main/services/posthog-plugin/service.test.ts b/apps/code/src/main/services/posthog-plugin/service.test.ts index b6db55f15..731deade7 100644 --- a/apps/code/src/main/services/posthog-plugin/service.test.ts +++ b/apps/code/src/main/services/posthog-plugin/service.test.ts @@ -37,15 +37,21 @@ vi.mock("node:fs/promises", async () => { return { ...fs.promises, default: fs.promises }; }); -vi.mock("../../utils/extract-zip.js", () => ({ - extractZip: mockExtractZip, -})); - -const mockFflateUnzipSync = vi.hoisted(() => vi.fn()); +const mockFflateUnzip = vi.hoisted(() => vi.fn()); vi.mock("fflate", () => ({ - unzipSync: mockFflateUnzipSync, + unzip: mockFflateUnzip, })); +vi.mock("../../utils/extract-zip.js", async () => { + const actual = await vi.importActual< + typeof import("../../utils/extract-zip.js") + >("../../utils/extract-zip.js"); + return { + ...actual, + extractZip: mockExtractZip, + }; +}); + vi.mock("node:os", () => ({ homedir: () => "/mock/home", tmpdir: () => "/mock/tmp", @@ -97,7 +103,7 @@ function simulateExtractZip() { mockExtractZip.mockImplementation( async (zipPath: string, extractDir: string) => { if (zipPath.includes("context-mill")) { - // Context-mill outer zip: produce omnibus-*.zip files (dummy bytes — unzipSync is mocked) + // Inner zip bytes are dummy — fflate.unzip is mocked below. vol.mkdirSync(extractDir, { recursive: true }); vol.writeFileSync(`${extractDir}/omnibus-test-skill.zip`, "dummy"); vol.writeFileSync(`${extractDir}/manifest.json`, "{}"); @@ -116,12 +122,18 @@ function simulateExtractZip() { }, ); - // Mock fflate unzipSync for inner zip extraction - mockFflateUnzipSync.mockImplementation(() => ({ - "SKILL.md": new TextEncoder().encode( - "---\nname: omnibus-test-skill\n---\n# Test Skill", - ), - })); + mockFflateUnzip.mockImplementation( + ( + _data: Uint8Array, + cb: (err: Error | null, data: Record) => void, + ) => { + cb(null, { + "SKILL.md": new TextEncoder().encode( + "---\nname: omnibus-test-skill\n---\n# Test Skill", + ), + }); + }, + ); } /** Create the bundled plugin directory in memfs */ diff --git a/apps/code/src/main/services/posthog-plugin/update-skills-saga.ts b/apps/code/src/main/services/posthog-plugin/update-skills-saga.ts index be0011979..5a1056a37 100644 --- a/apps/code/src/main/services/posthog-plugin/update-skills-saga.ts +++ b/apps/code/src/main/services/posthog-plugin/update-skills-saga.ts @@ -9,9 +9,8 @@ import { writeFile, } from "node:fs/promises"; import { basename, dirname, join } from "node:path"; -import { extractZip } from "@main/utils/extract-zip"; +import { extractZip, unzipAsync } from "@main/utils/extract-zip"; import { Saga } from "@posthog/shared"; -import { unzipSync } from "fflate"; /** * Overlays previously-downloaded skills on top of the runtime plugin dir. @@ -292,7 +291,7 @@ export class UpdateSkillsSaga extends Saga< const strippedName = file.replace(/^omnibus-/, "").replace(/\.zip$/, ""); const innerZipPath = join(extractDir, file); const innerZipData = await readFile(innerZipPath); - const innerEntries = unzipSync(new Uint8Array(innerZipData)); + const innerEntries = await unzipAsync(new Uint8Array(innerZipData)); const skillDestDir = join(destDir, strippedName); await mkdir(skillDestDir, { recursive: true }); diff --git a/apps/code/src/main/utils/extract-zip.ts b/apps/code/src/main/utils/extract-zip.ts index a27fa8527..4d9b501c2 100644 --- a/apps/code/src/main/utils/extract-zip.ts +++ b/apps/code/src/main/utils/extract-zip.ts @@ -1,6 +1,17 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import { dirname, join } from "node:path"; -import { unzipSync } from "fflate"; +import { type Unzipped, unzip } from "fflate"; + +// fflate's async unzip yields the event loop so the Electron main thread +// stays responsive on large archives. Do not switch back to unzipSync. +export function unzipAsync(data: Uint8Array): Promise { + return new Promise((resolve, reject) => { + unzip(data, (err, unzipped) => { + if (err) reject(err); + else resolve(unzipped); + }); + }); +} /** * Extracts a ZIP file to a directory using fflate (cross-platform, no native dependencies). @@ -10,7 +21,7 @@ export async function extractZip( extractDir: string, ): Promise { const data = await readFile(zipPath); - const unzipped = unzipSync(new Uint8Array(data)); + const unzipped = await unzipAsync(new Uint8Array(data)); for (const [filename, content] of Object.entries(unzipped)) { const fullPath = join(extractDir, filename); if (filename.endsWith("/")) {