FN-5988: fix desktop startup by lazy-loading archive parser deps

Prevent desktop startup failures by deferring archive parser dependencies until archive parsing runs.

- lazy-load `extract-zip` and `tar` inside archive parsing paths instead of at module import time
- add regression tests covering helper-only imports and dynamic loading for zip/tgz archive parsing
- include the desktop package's required @fusion/core runtime dependency groups in electron-builder packaging

Files changed:
 .../src/__tests__/agent-companies-parser.test.ts   | 115 +++++++++++++++++++++
 packages/core/src/agent-companies-parser.ts        |   4 +-
 packages/desktop/electron-builder.yml              |  44 ++++++++
 3 files changed, 161 insertions(+), 2 deletions(-)

Fusion-Task-Id: FN-5988
Fusion-Task-Lineage: b31bf43b-791c-4cd1-a62f-7095badcb706
This commit is contained in:
gsxdsm
2026-06-07 15:16:04 -07:00
parent fab8a62b55
commit ec8e4d3b0d
3 changed files with 161 additions and 2 deletions

View File

@@ -659,6 +659,121 @@ name: Nested Archive CEO
});
});
describe("lazy-load isolation", () => {
it("does not load extract-zip or tar for pure manifest helpers", async () => {
const extractZipModuleFactory = vi.fn(() => ({
default: vi.fn(async () => undefined),
}));
const tarModuleFactory = vi.fn(() => ({
x: vi.fn(async () => undefined),
}));
vi.resetModules();
vi.doMock("extract-zip", extractZipModuleFactory);
vi.doMock("tar", tarModuleFactory);
const parserModule = await import("../agent-companies-parser.js");
const parsed = parserModule.parseYamlFrontmatter(`---
name: Pure Helper
---
Body`);
expect(parsed.frontmatter.name).toBe("Pure Helper");
expect(parsed.body).toBe("Body");
const manifest = parserModule.parseAgentManifest(`---
name: Pure Agent
skills:
- review
---
Keep things tidy.`);
expect(manifest.name).toBe("Pure Agent");
const prepared = parserModule.prepareAgentCompaniesImport({
company: { name: "Pure Company" },
agents: [{ name: "Pure Agent", title: "Reviewer", instructionBody: "Keep things tidy." }],
teams: [],
projects: [],
tasks: [],
});
expect(prepared.items).toHaveLength(1);
const converted = parserModule.convertAgentCompanies({
company: { name: "Pure Company" },
agents: [{ name: "Pure Agent", title: "Reviewer", instructionBody: "Keep things tidy." }],
teams: [],
projects: [],
tasks: [],
skills: [],
});
expect(converted.inputs).toHaveLength(1);
expect(extractZipModuleFactory).not.toHaveBeenCalled();
expect(tarModuleFactory).not.toHaveBeenCalled();
vi.doUnmock("extract-zip");
vi.doUnmock("tar");
vi.resetModules();
});
it("dynamically imports extract-zip when parsing zip archives", async () => {
const extractZip = vi.fn(async (_archivePath: string, options: { dir: string }) => {
writeTextFile(join(options.dir, "zip-company", "COMPANY.md"), `---\nname: Mock Zip Company\n---`);
writeTextFile(join(options.dir, "zip-company", "agents", "ceo", "AGENTS.md"), `---\nname: Mock Zip CEO\n---`);
});
const extractZipModuleFactory = vi.fn(() => ({ default: extractZip }));
vi.resetModules();
vi.doMock("extract-zip", extractZipModuleFactory);
const parserModule = await import("../agent-companies-parser.js");
const root = createTempDir();
const archivePath = join(root, "company.zip");
writeFileSync(archivePath, Buffer.from("placeholder zip contents"));
const pkg = await parserModule.parseCompanyArchive(archivePath);
expect(pkg.company?.name).toBe("Mock Zip Company");
expect(pkg.agents[0]?.name).toBe("Mock Zip CEO");
expect(extractZipModuleFactory).toHaveBeenCalledTimes(1);
expect(extractZip).toHaveBeenCalledTimes(1);
expect(extractZip).toHaveBeenCalledWith(
archivePath,
expect.objectContaining({ dir: expect.any(String) }),
);
vi.doUnmock("extract-zip");
vi.resetModules();
});
it("dynamically imports tar when parsing tgz archives", async () => {
const tarExtract = vi.fn(async (options: { file: string; cwd: string }) => {
writeTextFile(join(options.cwd, "tar-company", "COMPANY.md"), `---\nname: Mock Tar Company\n---`);
writeTextFile(join(options.cwd, "tar-company", "agents", "ceo", "AGENTS.md"), `---\nname: Mock Tar CEO\n---`);
});
const tarModuleFactory = vi.fn(() => ({ x: tarExtract }));
vi.resetModules();
vi.doMock("tar", tarModuleFactory);
const parserModule = await import("../agent-companies-parser.js");
const root = createTempDir();
const archivePath = join(root, "company.tgz");
writeFileSync(archivePath, Buffer.from("placeholder tgz contents"));
const pkg = await parserModule.parseCompanyArchive(archivePath);
expect(pkg.company?.name).toBe("Mock Tar Company");
expect(pkg.agents[0]?.name).toBe("Mock Tar CEO");
expect(tarModuleFactory).toHaveBeenCalledTimes(1);
expect(tarExtract).toHaveBeenCalledTimes(1);
expect(tarExtract).toHaveBeenCalledWith(
expect.objectContaining({ file: archivePath, cwd: expect.any(String) }),
);
vi.doUnmock("tar");
vi.resetModules();
});
});
describe("conversion", () => {
it("maps AgentManifest to AgentCreateInput", () => {
const input = agentManifestToAgentCreateInput({

View File

@@ -8,8 +8,6 @@ import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync }
import { tmpdir } from "node:os";
import { isAbsolute, join, normalize, resolve } from "node:path";
import extractZip from "extract-zip";
import { x as tarExtract } from "tar";
import { parse as parseYaml } from "yaml";
import type {
@@ -481,6 +479,7 @@ function resolveExtractionRoot(tempDir: string): string {
}
async function extractTarArchive(archivePath: string, outputDir: string): Promise<void> {
const { x: tarExtract } = await import("tar");
await tarExtract({
file: archivePath,
cwd: outputDir,
@@ -530,6 +529,7 @@ export async function parseCompanyArchive(
if (resolvedArchivePath.endsWith(".tar.gz") || resolvedArchivePath.endsWith(".tgz")) {
await extractTarArchive(resolvedArchivePath, tempDir);
} else if (resolvedArchivePath.endsWith(".zip")) {
const extractZip = (await import("extract-zip")).default;
await extractZip(resolvedArchivePath, { dir: tempDir });
} else {
throw new AgentCompaniesParseError(