From 2287d1ec137cf847fdba4ec1af839a6fbc56f87e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 23:00:42 +0000 Subject: [PATCH] test: micro-optimize slow suites and CLI command setup --- src/cli/config-cli.test.ts | 15 +- .../register.option-collisions.test.ts | 15 +- .../gateway-cli/run.option-collisions.test.ts | 14 +- src/cli/nodes-cli.coverage.test.ts | 31 ++-- src/config/schema.test.ts | 142 +++++++------- .../bundled/session-memory/handler.test.ts | 32 +++- src/plugins/install.test.ts | 174 +++++++++++++----- 7 files changed, 259 insertions(+), 164 deletions(-) diff --git a/src/cli/config-cli.test.ts b/src/cli/config-cli.test.ts index b693e8b64..0e2ee4885 100644 --- a/src/cli/config-cli.test.ts +++ b/src/cli/config-cli.test.ts @@ -1,5 +1,5 @@ import { Command } from "commander"; -import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import type { ConfigFileSnapshot, OpenClawConfig } from "../config/types.js"; /** @@ -61,27 +61,24 @@ function setSnapshotOnce(snapshot: ConfigFileSnapshot) { } let registerConfigCli: typeof import("./config-cli.js").registerConfigCli; +let sharedProgram: Command; async function runConfigCommand(args: string[]) { - const program = new Command(); - program.exitOverride(); - registerConfigCli(program); - await program.parseAsync(args, { from: "user" }); + await sharedProgram.parseAsync(args, { from: "user" }); } describe("config cli", () => { beforeAll(async () => { ({ registerConfigCli } = await import("./config-cli.js")); + sharedProgram = new Command(); + sharedProgram.exitOverride(); + registerConfigCli(sharedProgram); }); beforeEach(() => { vi.clearAllMocks(); }); - afterEach(() => { - vi.restoreAllMocks(); - }); - describe("config set - issue #6070", () => { it("preserves existing config keys when setting a new value", async () => { const resolved: OpenClawConfig = { diff --git a/src/cli/gateway-cli/register.option-collisions.test.ts b/src/cli/gateway-cli/register.option-collisions.test.ts index a59c53ab1..d34300203 100644 --- a/src/cli/gateway-cli/register.option-collisions.test.ts +++ b/src/cli/gateway-cli/register.option-collisions.test.ts @@ -1,6 +1,5 @@ import { Command } from "commander"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { runRegisteredCli } from "../../test-utils/command-runner.js"; import { createCliRuntimeCapture } from "../test-runtime-capture.js"; const callGatewayCli = vi.fn(async (_method: string, _opts: unknown, _params?: unknown) => ({ @@ -113,9 +112,13 @@ vi.mock("./discover.js", () => ({ describe("gateway register option collisions", () => { let registerGatewayCli: typeof import("./register.js").registerGatewayCli; + let sharedProgram: Command; beforeAll(async () => { ({ registerGatewayCli } = await import("./register.js")); + sharedProgram = new Command(); + sharedProgram.exitOverride(); + registerGatewayCli(sharedProgram); }); beforeEach(() => { @@ -125,9 +128,8 @@ describe("gateway register option collisions", () => { }); it("forwards --token to gateway call when parent and child option names collide", async () => { - await runRegisteredCli({ - register: registerGatewayCli as (program: Command) => void, - argv: ["gateway", "call", "health", "--token", "tok_call", "--json"], + await sharedProgram.parseAsync(["gateway", "call", "health", "--token", "tok_call", "--json"], { + from: "user", }); expect(callGatewayCli).toHaveBeenCalledWith( @@ -140,9 +142,8 @@ describe("gateway register option collisions", () => { }); it("forwards --token to gateway probe when parent and child option names collide", async () => { - await runRegisteredCli({ - register: registerGatewayCli as (program: Command) => void, - argv: ["gateway", "probe", "--token", "tok_probe", "--json"], + await sharedProgram.parseAsync(["gateway", "probe", "--token", "tok_probe", "--json"], { + from: "user", }); expect(gatewayStatusCommand).toHaveBeenCalledWith( diff --git a/src/cli/gateway-cli/run.option-collisions.test.ts b/src/cli/gateway-cli/run.option-collisions.test.ts index 4fa6d7046..95245a919 100644 --- a/src/cli/gateway-cli/run.option-collisions.test.ts +++ b/src/cli/gateway-cli/run.option-collisions.test.ts @@ -1,6 +1,5 @@ import { Command } from "commander"; import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; -import { runRegisteredCli } from "../../test-utils/command-runner.js"; import { createCliRuntimeCapture } from "../test-runtime-capture.js"; const startGatewayServer = vi.fn(async (_port: number, _opts?: unknown) => ({ @@ -93,9 +92,14 @@ vi.mock("./run-loop.js", () => ({ describe("gateway run option collisions", () => { let addGatewayRunCommand: typeof import("./run.js").addGatewayRunCommand; + let sharedProgram: Command; beforeAll(async () => { ({ addGatewayRunCommand } = await import("./run.js")); + sharedProgram = new Command(); + sharedProgram.exitOverride(); + const gateway = addGatewayRunCommand(sharedProgram.command("gateway")); + addGatewayRunCommand(gateway.command("run")); }); beforeEach(() => { @@ -109,13 +113,7 @@ describe("gateway run option collisions", () => { }); async function runGatewayCli(argv: string[]) { - await runRegisteredCli({ - register: ((program: Command) => { - const gateway = addGatewayRunCommand(program.command("gateway")); - addGatewayRunCommand(gateway.command("run")); - }) as (program: Command) => void, - argv, - }); + await sharedProgram.parseAsync(argv, { from: "user" }); } function expectAuthOverrideMode(mode: string) { diff --git a/src/cli/nodes-cli.coverage.test.ts b/src/cli/nodes-cli.coverage.test.ts index f66373a52..3a10b43b7 100644 --- a/src/cli/nodes-cli.coverage.test.ts +++ b/src/cli/nodes-cli.coverage.test.ts @@ -12,6 +12,9 @@ type NodeInvokeCall = { }; }; +let lastNodeInvokeCall: NodeInvokeCall | null = null; +let lastApprovalRequestCall: { params?: Record } | null = null; + const callGateway = vi.fn(async (opts: NodeInvokeCall) => { if (opts.method === "node.list") { return { @@ -28,6 +31,7 @@ const callGateway = vi.fn(async (opts: NodeInvokeCall) => { }; } if (opts.method === "node.invoke") { + lastNodeInvokeCall = opts; const command = opts.params?.command; if (command === "system.run.prepare") { const params = (opts.params?.params ?? {}) as { @@ -83,6 +87,7 @@ const callGateway = vi.fn(async (opts: NodeInvokeCall) => { }; } if (opts.method === "exec.approval.request") { + lastApprovalRequestCall = opts as { params?: Record }; return { decision: "allow-once" }; } return { ok: true }; @@ -107,44 +112,36 @@ vi.mock("../config/config.js", () => ({ describe("nodes-cli coverage", () => { let registerNodesCli: (program: Command) => void; + let sharedProgram: Command; const getNodeInvokeCall = () => { - const nodeInvokeCalls = callGateway.mock.calls - .map((call) => call[0]) - .filter((entry): entry is NodeInvokeCall => entry?.method === "node.invoke"); - const last = nodeInvokeCalls.at(-1); + const last = lastNodeInvokeCall; if (!last) { throw new Error("expected node.invoke call"); } return last; }; - const getApprovalRequestCall = () => - callGateway.mock.calls.find((call) => call[0]?.method === "exec.approval.request")?.[0] as { - params?: Record; - }; - - const createNodesProgram = () => { - const program = new Command(); - program.exitOverride(); - registerNodesCli(program); - return program; - }; + const getApprovalRequestCall = () => lastApprovalRequestCall; const runNodesCommand = async (args: string[]) => { - const program = createNodesProgram(); - await program.parseAsync(args, { from: "user" }); + await sharedProgram.parseAsync(args, { from: "user" }); return getNodeInvokeCall(); }; beforeAll(async () => { ({ registerNodesCli } = await import("./nodes-cli.js")); + sharedProgram = new Command(); + sharedProgram.exitOverride(); + registerNodesCli(sharedProgram); }); beforeEach(() => { resetRuntimeCapture(); callGateway.mockClear(); randomIdempotencyKey.mockClear(); + lastNodeInvokeCall = null; + lastApprovalRequestCall = null; }); it("invokes system.run with parsed params", async () => { diff --git a/src/config/schema.test.ts b/src/config/schema.test.ts index c132040d9..3314543d5 100644 --- a/src/config/schema.test.ts +++ b/src/config/schema.test.ts @@ -3,28 +3,17 @@ import { buildConfigSchema } from "./schema.js"; import { applyDerivedTags, CONFIG_TAGS, deriveTagsForPath } from "./schema.tags.js"; describe("config schema", () => { + type SchemaInput = NonNullable[0]>; let baseSchema: ReturnType; + let pluginUiHintInput: SchemaInput; + let tokenHintInput: SchemaInput; + let mergedSchemaInput: SchemaInput; + let heartbeatChannelInput: SchemaInput; + let cachedMergeInput: SchemaInput; beforeAll(() => { baseSchema = buildConfigSchema(); - }); - - it("exports schema + hints", () => { - const res = baseSchema; - const schema = res.schema as { properties?: Record }; - expect(schema.properties?.gateway).toBeTruthy(); - expect(schema.properties?.agents).toBeTruthy(); - expect(schema.properties?.acp).toBeTruthy(); - expect(schema.properties?.$schema).toBeUndefined(); - expect(res.uiHints.gateway?.label).toBe("Gateway"); - expect(res.uiHints["gateway.auth.token"]?.sensitive).toBe(true); - expect(res.uiHints["channels.discord.threadBindings.spawnAcpSessions"]?.label).toBeTruthy(); - expect(res.version).toBeTruthy(); - expect(res.generatedAt).toBeTruthy(); - }); - - it("merges plugin ui hints", () => { - const res = buildConfigSchema({ + pluginUiHintInput = { plugins: [ { id: "voice-call", @@ -36,18 +25,8 @@ describe("config schema", () => { }, }, ], - }); - - expect(res.uiHints["plugins.entries.voice-call"]?.label).toBe("Voice Call"); - expect(res.uiHints["plugins.entries.voice-call.config"]?.label).toBe("Voice Call Config"); - expect(res.uiHints["plugins.entries.voice-call.config.twilio.authToken"]?.label).toBe( - "Auth Token", - ); - expect(res.uiHints["plugins.entries.voice-call.config.twilio.authToken"]?.sensitive).toBe(true); - }); - - it("does not re-mark existing non-sensitive token-like fields", () => { - const res = buildConfigSchema({ + }; + tokenHintInput = { plugins: [ { id: "voice-call", @@ -56,13 +35,8 @@ describe("config schema", () => { }, }, ], - }); - - expect(res.uiHints["plugins.entries.voice-call.config.tokens"]?.sensitive).toBe(false); - }); - - it("merges plugin + channel schemas", () => { - const res = buildConfigSchema({ + }; + mergedSchemaInput = { plugins: [ { id: "voice-call", @@ -87,7 +61,67 @@ describe("config schema", () => { }, }, ], - }); + }; + heartbeatChannelInput = { + channels: [ + { + id: "bluebubbles", + label: "BlueBubbles", + configSchema: { type: "object" }, + }, + ], + }; + cachedMergeInput = { + plugins: [ + { + id: "voice-call", + name: "Voice Call", + configSchema: { type: "object", properties: { provider: { type: "string" } } }, + }, + ], + channels: [ + { + id: "matrix", + label: "Matrix", + configSchema: { type: "object", properties: { accessToken: { type: "string" } } }, + }, + ], + }; + }); + + it("exports schema + hints", () => { + const res = baseSchema; + const schema = res.schema as { properties?: Record }; + expect(schema.properties?.gateway).toBeTruthy(); + expect(schema.properties?.agents).toBeTruthy(); + expect(schema.properties?.acp).toBeTruthy(); + expect(schema.properties?.$schema).toBeUndefined(); + expect(res.uiHints.gateway?.label).toBe("Gateway"); + expect(res.uiHints["gateway.auth.token"]?.sensitive).toBe(true); + expect(res.uiHints["channels.discord.threadBindings.spawnAcpSessions"]?.label).toBeTruthy(); + expect(res.version).toBeTruthy(); + expect(res.generatedAt).toBeTruthy(); + }); + + it("merges plugin ui hints", () => { + const res = buildConfigSchema(pluginUiHintInput); + + expect(res.uiHints["plugins.entries.voice-call"]?.label).toBe("Voice Call"); + expect(res.uiHints["plugins.entries.voice-call.config"]?.label).toBe("Voice Call Config"); + expect(res.uiHints["plugins.entries.voice-call.config.twilio.authToken"]?.label).toBe( + "Auth Token", + ); + expect(res.uiHints["plugins.entries.voice-call.config.twilio.authToken"]?.sensitive).toBe(true); + }); + + it("does not re-mark existing non-sensitive token-like fields", () => { + const res = buildConfigSchema(tokenHintInput); + + expect(res.uiHints["plugins.entries.voice-call.config.tokens"]?.sensitive).toBe(false); + }); + + it("merges plugin + channel schemas", () => { + const res = buildConfigSchema(mergedSchemaInput); const schema = res.schema as { properties?: Record; @@ -110,15 +144,7 @@ describe("config schema", () => { }); it("adds heartbeat target hints with dynamic channels", () => { - const res = buildConfigSchema({ - channels: [ - { - id: "bluebubbles", - label: "BlueBubbles", - configSchema: { type: "object" }, - }, - ], - }); + const res = buildConfigSchema(heartbeatChannelInput); const defaultsHint = res.uiHints["agents.defaults.heartbeat.target"]; const listHint = res.uiHints["agents.list.*.heartbeat.target"]; @@ -128,26 +154,10 @@ describe("config schema", () => { }); it("caches merged schemas for identical plugin/channel metadata", () => { - const params = { - plugins: [ - { - id: "voice-call", - name: "Voice Call", - configSchema: { type: "object", properties: { provider: { type: "string" } } }, - }, - ], - channels: [ - { - id: "matrix", - label: "Matrix", - configSchema: { type: "object", properties: { accessToken: { type: "string" } } }, - }, - ], - }; - const first = buildConfigSchema(params); + const first = buildConfigSchema(cachedMergeInput); const second = buildConfigSchema({ - plugins: [{ ...params.plugins[0] }], - channels: [{ ...params.channels[0] }], + plugins: [{ ...cachedMergeInput.plugins![0] }], + channels: [{ ...cachedMergeInput.channels![0] }], }); expect(second).toBe(first); }); diff --git a/src/hooks/bundled/session-memory/handler.test.ts b/src/hooks/bundled/session-memory/handler.test.ts index 0b2b10eb0..7f29c58b1 100644 --- a/src/hooks/bundled/session-memory/handler.test.ts +++ b/src/hooks/bundled/session-memory/handler.test.ts @@ -1,8 +1,9 @@ import fs from "node:fs/promises"; +import os from "node:os"; import path from "node:path"; -import { beforeAll, describe, expect, it, vi } from "vitest"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import type { OpenClawConfig } from "../../../config/config.js"; -import { makeTempWorkspace, writeWorkspaceFile } from "../../../test-helpers/workspace.js"; +import { writeWorkspaceFile } from "../../../test-helpers/workspace.js"; import type { HookHandler } from "../../hooks.js"; import { createHookEvent } from "../../hooks.js"; @@ -12,9 +13,28 @@ vi.mock("../../llm-slug-generator.js", () => ({ })); let handler: HookHandler; +let suiteWorkspaceRoot = ""; +let workspaceCaseCounter = 0; + +async function createCaseWorkspace(prefix = "case"): Promise { + const dir = path.join(suiteWorkspaceRoot, `${prefix}-${workspaceCaseCounter}`); + workspaceCaseCounter += 1; + await fs.mkdir(dir, { recursive: true }); + return dir; +} beforeAll(async () => { ({ default: handler } = await import("./handler.js")); + suiteWorkspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-session-memory-")); +}); + +afterAll(async () => { + if (!suiteWorkspaceRoot) { + return; + } + await fs.rm(suiteWorkspaceRoot, { recursive: true, force: true }); + suiteWorkspaceRoot = ""; + workspaceCaseCounter = 0; }); /** @@ -69,7 +89,7 @@ async function runNewWithPreviousSession(params: { cfg?: (tempDir: string) => OpenClawConfig; action?: "new" | "reset"; }): Promise<{ tempDir: string; files: string[]; memoryContent: string }> { - const tempDir = await makeTempWorkspace("openclaw-session-memory-"); + const tempDir = await createCaseWorkspace("workspace"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -117,7 +137,7 @@ function makeSessionMemoryConfig(tempDir: string, messages?: number): OpenClawCo async function createSessionMemoryWorkspace(params?: { activeSession?: { name: string; content: string }; }): Promise<{ tempDir: string; sessionsDir: string; activeSessionFile?: string }> { - const tempDir = await makeTempWorkspace("openclaw-session-memory-"); + const tempDir = await createCaseWorkspace("workspace"); const sessionsDir = path.join(tempDir, "sessions"); await fs.mkdir(sessionsDir, { recursive: true }); @@ -162,7 +182,7 @@ function expectMemoryConversation(params: { describe("session-memory hook", () => { it("skips non-command events", async () => { - const tempDir = await makeTempWorkspace("openclaw-session-memory-"); + const tempDir = await createCaseWorkspace("workspace"); const event = createHookEvent("agent", "bootstrap", "agent:main:main", { workspaceDir: tempDir, @@ -176,7 +196,7 @@ describe("session-memory hook", () => { }); it("skips commands other than new", async () => { - const tempDir = await makeTempWorkspace("openclaw-session-memory-"); + const tempDir = await createCaseWorkspace("workspace"); const event = createHookEvent("command", "help", "agent:main:main", { workspaceDir: tempDir, diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index 7751e0b1e..c1078e05a 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -22,8 +22,13 @@ let installPluginFromPath: typeof import("./install.js").installPluginFromPath; let PLUGIN_INSTALL_ERROR_CODE: typeof import("./install.js").PLUGIN_INSTALL_ERROR_CODE; let runCommandWithTimeout: typeof import("../process/exec.js").runCommandWithTimeout; let suiteTempRoot = ""; +let suiteFixtureRoot = ""; let tempDirCounter = 0; const pluginFixturesDir = path.resolve(process.cwd(), "test", "fixtures", "plugins-install"); +const archiveFixturePathCache = new Map(); +const dynamicArchiveTemplatePathCache = new Map(); +let installPluginFromDirTemplateDir = ""; +let manifestInstallTemplateDir = ""; function ensureSuiteTempRoot() { if (suiteTempRoot) { @@ -40,6 +45,15 @@ function makeTempDir() { return dir; } +function ensureSuiteFixtureRoot() { + if (suiteFixtureRoot) { + return suiteFixtureRoot; + } + suiteFixtureRoot = path.join(ensureSuiteTempRoot(), "_fixtures"); + fs.mkdirSync(suiteFixtureRoot, { recursive: true }); + return suiteFixtureRoot; +} + async function packToArchive({ pkgDir, outDir, @@ -66,10 +80,18 @@ async function createVoiceCallArchiveBuffer(version: string): Promise { return fs.readFileSync(path.join(pluginFixturesDir, `voice-call-${version}.tgz`)); } -function writeArchiveBuffer(params: { outName: string; buffer: Buffer }): string { - const workDir = makeTempDir(); - const archivePath = path.join(workDir, params.outName); +function getArchiveFixturePath(params: { + cacheKey: string; + outName: string; + buffer: Buffer; +}): string { + const hit = archiveFixturePathCache.get(params.cacheKey); + if (hit) { + return hit; + } + const archivePath = path.join(ensureSuiteFixtureRoot(), params.outName); fs.writeFileSync(archivePath, params.buffer); + archiveFixturePathCache.set(params.cacheKey, archivePath); return archivePath; } @@ -94,7 +116,11 @@ async function getVoiceCallArchiveBuffer(version: string): Promise { async function setupVoiceCallArchiveInstall(params: { outName: string; version: string }) { const stateDir = makeTempDir(); const archiveBuffer = await getVoiceCallArchiveBuffer(params.version); - const archivePath = writeArchiveBuffer({ outName: params.outName, buffer: archiveBuffer }); + const archivePath = getArchiveFixturePath({ + cacheKey: `voice-call:${params.version}`, + outName: params.outName, + buffer: archiveBuffer, + }); return { stateDir, archivePath, @@ -131,22 +157,17 @@ function setupPluginInstallDirs() { } function setupInstallPluginFromDirFixture(params?: { devDependencies?: Record }) { - const workDir = makeTempDir(); const stateDir = makeTempDir(); - const pluginDir = path.join(workDir, "plugin"); - fs.mkdirSync(path.join(pluginDir, "dist"), { recursive: true }); - fs.writeFileSync( - path.join(pluginDir, "package.json"), - JSON.stringify({ - name: "@openclaw/test-plugin", - version: "0.0.1", - openclaw: { extensions: ["./dist/index.js"] }, - dependencies: { "left-pad": "1.3.0" }, - ...(params?.devDependencies ? { devDependencies: params.devDependencies } : {}), - }), - "utf-8", - ); - fs.writeFileSync(path.join(pluginDir, "dist", "index.js"), "export {};", "utf-8"); + const pluginDir = path.join(makeTempDir(), "plugin"); + fs.cpSync(installPluginFromDirTemplateDir, pluginDir, { recursive: true }); + if (params?.devDependencies) { + const packageJsonPath = path.join(pluginDir, "package.json"); + const manifest = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as { + devDependencies?: Record; + }; + manifest.devDependencies = params.devDependencies; + fs.writeFileSync(packageJsonPath, JSON.stringify(manifest), "utf-8"); + } return { pluginDir, extensionsDir: path.join(stateDir, "extensions") }; } @@ -164,18 +185,9 @@ async function installFromDirWithWarnings(params: { pluginDir: string; extension } function setupManifestInstallFixture(params: { manifestId: string }) { - const { pluginDir, extensionsDir } = setupPluginInstallDirs(); - fs.mkdirSync(path.join(pluginDir, "dist"), { recursive: true }); - fs.writeFileSync( - path.join(pluginDir, "package.json"), - JSON.stringify({ - name: "@openclaw/cognee-openclaw", - version: "0.0.1", - openclaw: { extensions: ["./dist/index.js"] }, - }), - "utf-8", - ); - fs.writeFileSync(path.join(pluginDir, "dist", "index.js"), "export {};", "utf-8"); + const stateDir = makeTempDir(); + const pluginDir = path.join(makeTempDir(), "plugin-src"); + fs.cpSync(manifestInstallTemplateDir, pluginDir, { recursive: true }); fs.writeFileSync( path.join(pluginDir, "openclaw.plugin.json"), JSON.stringify({ @@ -184,7 +196,7 @@ function setupManifestInstallFixture(params: { manifestId: string }) { }), "utf-8", ); - return { pluginDir, extensionsDir }; + return { pluginDir, extensionsDir: path.join(stateDir, "extensions") }; } async function expectArchiveInstallReservedSegmentRejection(params: { @@ -214,20 +226,31 @@ async function installArchivePackageAndReturnResult(params: { withDistIndex?: boolean; }) { const stateDir = makeTempDir(); - const workDir = makeTempDir(); - const pkgDir = path.join(workDir, "package"); - fs.mkdirSync(pkgDir, { recursive: true }); - if (params.withDistIndex) { - fs.mkdirSync(path.join(pkgDir, "dist"), { recursive: true }); - fs.writeFileSync(path.join(pkgDir, "dist", "index.js"), "export {};", "utf-8"); - } - fs.writeFileSync(path.join(pkgDir, "package.json"), JSON.stringify(params.packageJson), "utf-8"); - - const archivePath = await packToArchive({ - pkgDir, - outDir: workDir, - outName: params.outName, + const templateKey = JSON.stringify({ + packageJson: params.packageJson, + withDistIndex: params.withDistIndex === true, }); + let archivePath = dynamicArchiveTemplatePathCache.get(templateKey); + if (!archivePath) { + const templateDir = makeTempDir(); + const pkgDir = path.join(templateDir, "package"); + fs.mkdirSync(pkgDir, { recursive: true }); + if (params.withDistIndex) { + fs.mkdirSync(path.join(pkgDir, "dist"), { recursive: true }); + fs.writeFileSync(path.join(pkgDir, "dist", "index.js"), "export {};", "utf-8"); + } + fs.writeFileSync( + path.join(pkgDir, "package.json"), + JSON.stringify(params.packageJson), + "utf-8", + ); + archivePath = await packToArchive({ + pkgDir, + outDir: ensureSuiteFixtureRoot(), + outName: params.outName, + }); + dynamicArchiveTemplatePathCache.set(templateKey, archivePath); + } const extensionsDir = path.join(stateDir, "extensions"); const result = await installPluginFromArchive({ @@ -258,6 +281,52 @@ beforeAll(async () => { PLUGIN_INSTALL_ERROR_CODE, } = await import("./install.js")); ({ runCommandWithTimeout } = await import("../process/exec.js")); + + installPluginFromDirTemplateDir = path.join( + ensureSuiteFixtureRoot(), + "install-from-dir-template", + ); + fs.mkdirSync(path.join(installPluginFromDirTemplateDir, "dist"), { recursive: true }); + fs.writeFileSync( + path.join(installPluginFromDirTemplateDir, "package.json"), + JSON.stringify({ + name: "@openclaw/test-plugin", + version: "0.0.1", + openclaw: { extensions: ["./dist/index.js"] }, + dependencies: { "left-pad": "1.3.0" }, + }), + "utf-8", + ); + fs.writeFileSync( + path.join(installPluginFromDirTemplateDir, "dist", "index.js"), + "export {};", + "utf-8", + ); + + manifestInstallTemplateDir = path.join(ensureSuiteFixtureRoot(), "manifest-install-template"); + fs.mkdirSync(path.join(manifestInstallTemplateDir, "dist"), { recursive: true }); + fs.writeFileSync( + path.join(manifestInstallTemplateDir, "package.json"), + JSON.stringify({ + name: "@openclaw/cognee-openclaw", + version: "0.0.1", + openclaw: { extensions: ["./dist/index.js"] }, + }), + "utf-8", + ); + fs.writeFileSync( + path.join(manifestInstallTemplateDir, "dist", "index.js"), + "export {};", + "utf-8", + ); + fs.writeFileSync( + path.join(manifestInstallTemplateDir, "openclaw.plugin.json"), + JSON.stringify({ + id: "manifest-template", + configSchema: { type: "object", properties: {} }, + }), + "utf-8", + ); }); beforeEach(() => { @@ -303,8 +372,9 @@ describe("installPluginFromArchive", () => { it("installs from a zip archive", async () => { const stateDir = makeTempDir(); - const archivePath = writeArchiveBuffer({ - outName: "plugin.zip", + const archivePath = getArchiveFixturePath({ + cacheKey: "zipper:0.0.1", + outName: "zipper-0.0.1.zip", buffer: await ZIPPER_ARCHIVE_BUFFER_PROMISE, }); @@ -318,12 +388,14 @@ describe("installPluginFromArchive", () => { it("allows updates when mode is update", async () => { const stateDir = makeTempDir(); - const archiveV1 = writeArchiveBuffer({ - outName: "plugin-v1.tgz", + const archiveV1 = getArchiveFixturePath({ + cacheKey: "voice-call:0.0.1", + outName: "voice-call-0.0.1.tgz", buffer: await VOICE_CALL_ARCHIVE_V1_BUFFER_PROMISE, }); - const archiveV2 = writeArchiveBuffer({ - outName: "plugin-v2.tgz", + const archiveV2 = getArchiveFixturePath({ + cacheKey: "voice-call:0.0.2", + outName: "voice-call-0.0.2.tgz", buffer: await VOICE_CALL_ARCHIVE_V2_BUFFER_PROMISE, });