mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 17:24:58 +00:00
perf(test): cache hook installer fixtures
This commit is contained in:
@@ -1,14 +1,23 @@
|
|||||||
import JSZip from "jszip";
|
import JSZip from "jszip";
|
||||||
import { randomUUID } from "node:crypto";
|
import { randomUUID } from "node:crypto";
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
|
import fsPromises from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import * as tar from "tar";
|
import * as tar from "tar";
|
||||||
import { afterAll, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
const fixtureRoot = path.join(os.tmpdir(), `openclaw-hook-install-${randomUUID()}`);
|
const fixtureRoot = path.join(os.tmpdir(), `openclaw-hook-install-${randomUUID()}`);
|
||||||
let tempDirIndex = 0;
|
let tempDirIndex = 0;
|
||||||
|
|
||||||
|
let zipHooksBuffer: Buffer;
|
||||||
|
let zipTraversalBuffer: Buffer;
|
||||||
|
let tarHooksBuffer: Buffer;
|
||||||
|
let tarTraversalBuffer: Buffer;
|
||||||
|
let tarEvilIdBuffer: Buffer;
|
||||||
|
let tarReservedIdBuffer: Buffer;
|
||||||
|
let npmPackHooksBuffer: Buffer;
|
||||||
|
|
||||||
vi.mock("../process/exec.js", () => ({
|
vi.mock("../process/exec.js", () => ({
|
||||||
runCommandWithTimeout: vi.fn(),
|
runCommandWithTimeout: vi.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -45,6 +54,139 @@ const { runCommandWithTimeout } = await import("../process/exec.js");
|
|||||||
const { installHooksFromArchive, installHooksFromNpmSpec, installHooksFromPath } =
|
const { installHooksFromArchive, installHooksFromNpmSpec, installHooksFromPath } =
|
||||||
await import("./install.js");
|
await import("./install.js");
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
fs.mkdirSync(fixtureRoot, { recursive: true });
|
||||||
|
|
||||||
|
const zipHooks = new JSZip();
|
||||||
|
zipHooks.file(
|
||||||
|
"package/package.json",
|
||||||
|
JSON.stringify({
|
||||||
|
name: "@openclaw/zip-hooks",
|
||||||
|
version: "0.0.1",
|
||||||
|
openclaw: { hooks: ["./hooks/zip-hook"] },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
zipHooks.file(
|
||||||
|
"package/hooks/zip-hook/HOOK.md",
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"name: zip-hook",
|
||||||
|
"description: Zip hook",
|
||||||
|
'metadata: {"openclaw":{"events":["command:new"]}}',
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"# Zip Hook",
|
||||||
|
].join("\n"),
|
||||||
|
);
|
||||||
|
zipHooks.file("package/hooks/zip-hook/handler.ts", "export default async () => {};\n");
|
||||||
|
zipHooksBuffer = await zipHooks.generateAsync({ type: "nodebuffer" });
|
||||||
|
|
||||||
|
const zipTraversal = new JSZip();
|
||||||
|
zipTraversal.file("../pwned.txt", "nope\n");
|
||||||
|
zipTraversalBuffer = await zipTraversal.generateAsync({ type: "nodebuffer" });
|
||||||
|
|
||||||
|
const makeTarWithPackage = async (params: {
|
||||||
|
packageName: string;
|
||||||
|
hookName: string;
|
||||||
|
}): Promise<Buffer> => {
|
||||||
|
const workDir = makeTempDir();
|
||||||
|
const archivePath = path.join(workDir, "hooks.tar");
|
||||||
|
const pkgDir = path.join(workDir, "package");
|
||||||
|
fs.mkdirSync(path.join(pkgDir, "hooks", params.hookName), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "package.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
name: params.packageName,
|
||||||
|
version: "0.0.1",
|
||||||
|
openclaw: { hooks: [`./hooks/${params.hookName}`] },
|
||||||
|
}),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "hooks", params.hookName, "HOOK.md"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
`name: ${params.hookName}`,
|
||||||
|
`description: ${params.hookName}`,
|
||||||
|
'metadata: {"openclaw":{"events":["command:new"]}}',
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"# Hook",
|
||||||
|
].join("\n"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "hooks", params.hookName, "handler.ts"),
|
||||||
|
"export default async () => {};\n",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
||||||
|
return await fsPromises.readFile(archivePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
tarHooksBuffer = await makeTarWithPackage({
|
||||||
|
packageName: "@openclaw/tar-hooks",
|
||||||
|
hookName: "tar-hook",
|
||||||
|
});
|
||||||
|
tarEvilIdBuffer = await makeTarWithPackage({ packageName: "@evil/..", hookName: "evil-hook" });
|
||||||
|
tarReservedIdBuffer = await makeTarWithPackage({
|
||||||
|
packageName: "@evil/.",
|
||||||
|
hookName: "reserved-hook",
|
||||||
|
});
|
||||||
|
|
||||||
|
const makeTraversalTar = async (): Promise<Buffer> => {
|
||||||
|
const workDir = makeTempDir();
|
||||||
|
const insideDir = path.join(workDir, "inside");
|
||||||
|
fs.mkdirSync(insideDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(workDir, "outside.txt"), "nope\n", "utf-8");
|
||||||
|
const archivePath = path.join(workDir, "traversal.tar");
|
||||||
|
await tar.c({ cwd: insideDir, file: archivePath }, ["../outside.txt"]);
|
||||||
|
return await fsPromises.readFile(archivePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
tarTraversalBuffer = await makeTraversalTar();
|
||||||
|
|
||||||
|
const makeNpmPackTgz = async (): Promise<Buffer> => {
|
||||||
|
const workDir = makeTempDir();
|
||||||
|
const packedName = "test-hooks-0.0.1.tgz";
|
||||||
|
const pkgDir = path.join(workDir, "package");
|
||||||
|
fs.mkdirSync(path.join(pkgDir, "hooks", "one-hook"), { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "package.json"),
|
||||||
|
JSON.stringify({
|
||||||
|
name: "@openclaw/test-hooks",
|
||||||
|
version: "0.0.1",
|
||||||
|
openclaw: { hooks: ["./hooks/one-hook"] },
|
||||||
|
}),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "hooks", "one-hook", "HOOK.md"),
|
||||||
|
[
|
||||||
|
"---",
|
||||||
|
"name: one-hook",
|
||||||
|
"description: One hook",
|
||||||
|
'metadata: {"openclaw":{"events":["command:new"]}}',
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"# One Hook",
|
||||||
|
].join("\n"),
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(pkgDir, "hooks", "one-hook", "handler.ts"),
|
||||||
|
"export default async () => {};\n",
|
||||||
|
"utf-8",
|
||||||
|
);
|
||||||
|
|
||||||
|
const packTmpDir = makeTempDir();
|
||||||
|
const archivePath = await packToArchive({ pkgDir, outDir: packTmpDir, outName: packedName });
|
||||||
|
return await fsPromises.readFile(archivePath);
|
||||||
|
};
|
||||||
|
|
||||||
|
npmPackHooksBuffer = await makeNpmPackTgz();
|
||||||
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
try {
|
try {
|
||||||
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||||
@@ -62,31 +204,7 @@ describe("installHooksFromArchive", () => {
|
|||||||
const stateDir = makeTempDir();
|
const stateDir = makeTempDir();
|
||||||
const workDir = makeTempDir();
|
const workDir = makeTempDir();
|
||||||
const archivePath = path.join(workDir, "hooks.zip");
|
const archivePath = path.join(workDir, "hooks.zip");
|
||||||
|
fs.writeFileSync(archivePath, zipHooksBuffer);
|
||||||
const zip = new JSZip();
|
|
||||||
zip.file(
|
|
||||||
"package/package.json",
|
|
||||||
JSON.stringify({
|
|
||||||
name: "@openclaw/zip-hooks",
|
|
||||||
version: "0.0.1",
|
|
||||||
openclaw: { hooks: ["./hooks/zip-hook"] },
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
zip.file(
|
|
||||||
"package/hooks/zip-hook/HOOK.md",
|
|
||||||
[
|
|
||||||
"---",
|
|
||||||
"name: zip-hook",
|
|
||||||
"description: Zip hook",
|
|
||||||
'metadata: {"openclaw":{"events":["command:new"]}}',
|
|
||||||
"---",
|
|
||||||
"",
|
|
||||||
"# Zip Hook",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
zip.file("package/hooks/zip-hook/handler.ts", "export default async () => {};\n");
|
|
||||||
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
||||||
fs.writeFileSync(archivePath, buffer);
|
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
@@ -105,11 +223,7 @@ describe("installHooksFromArchive", () => {
|
|||||||
const stateDir = makeTempDir();
|
const stateDir = makeTempDir();
|
||||||
const workDir = makeTempDir();
|
const workDir = makeTempDir();
|
||||||
const archivePath = path.join(workDir, "traversal.zip");
|
const archivePath = path.join(workDir, "traversal.zip");
|
||||||
|
fs.writeFileSync(archivePath, zipTraversalBuffer);
|
||||||
const zip = new JSZip();
|
|
||||||
zip.file("../pwned.txt", "nope\n");
|
|
||||||
const buffer = await zip.generateAsync({ type: "nodebuffer" });
|
|
||||||
fs.writeFileSync(archivePath, buffer);
|
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
@@ -126,37 +240,7 @@ describe("installHooksFromArchive", () => {
|
|||||||
const stateDir = makeTempDir();
|
const stateDir = makeTempDir();
|
||||||
const workDir = makeTempDir();
|
const workDir = makeTempDir();
|
||||||
const archivePath = path.join(workDir, "hooks.tar");
|
const archivePath = path.join(workDir, "hooks.tar");
|
||||||
const pkgDir = path.join(workDir, "package");
|
fs.writeFileSync(archivePath, tarHooksBuffer);
|
||||||
|
|
||||||
fs.mkdirSync(path.join(pkgDir, "hooks", "tar-hook"), { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "package.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
name: "@openclaw/tar-hooks",
|
|
||||||
version: "0.0.1",
|
|
||||||
openclaw: { hooks: ["./hooks/tar-hook"] },
|
|
||||||
}),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "hooks", "tar-hook", "HOOK.md"),
|
|
||||||
[
|
|
||||||
"---",
|
|
||||||
"name: tar-hook",
|
|
||||||
"description: Tar hook",
|
|
||||||
'metadata: {"openclaw":{"events":["command:new"]}}',
|
|
||||||
"---",
|
|
||||||
"",
|
|
||||||
"# Tar Hook",
|
|
||||||
].join("\n"),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "hooks", "tar-hook", "handler.ts"),
|
|
||||||
"export default async () => {};\n",
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
@@ -173,13 +257,8 @@ describe("installHooksFromArchive", () => {
|
|||||||
it("rejects tar archives with traversal entries", async () => {
|
it("rejects tar archives with traversal entries", async () => {
|
||||||
const stateDir = makeTempDir();
|
const stateDir = makeTempDir();
|
||||||
const workDir = makeTempDir();
|
const workDir = makeTempDir();
|
||||||
const insideDir = path.join(workDir, "inside");
|
|
||||||
fs.mkdirSync(insideDir, { recursive: true });
|
|
||||||
|
|
||||||
// Create a tar that contains a ../ entry; extract must fail closed.
|
|
||||||
fs.writeFileSync(path.join(workDir, "outside.txt"), "nope\n", "utf-8");
|
|
||||||
const archivePath = path.join(workDir, "traversal.tar");
|
const archivePath = path.join(workDir, "traversal.tar");
|
||||||
await tar.c({ cwd: insideDir, file: archivePath }, ["../outside.txt"]);
|
fs.writeFileSync(archivePath, tarTraversalBuffer);
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
@@ -196,37 +275,7 @@ describe("installHooksFromArchive", () => {
|
|||||||
const stateDir = makeTempDir();
|
const stateDir = makeTempDir();
|
||||||
const workDir = makeTempDir();
|
const workDir = makeTempDir();
|
||||||
const archivePath = path.join(workDir, "hooks.tar");
|
const archivePath = path.join(workDir, "hooks.tar");
|
||||||
const pkgDir = path.join(workDir, "package");
|
fs.writeFileSync(archivePath, tarEvilIdBuffer);
|
||||||
|
|
||||||
fs.mkdirSync(path.join(pkgDir, "hooks", "evil-hook"), { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "package.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
name: "@evil/..",
|
|
||||||
version: "0.0.1",
|
|
||||||
openclaw: { hooks: ["./hooks/evil-hook"] },
|
|
||||||
}),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "hooks", "evil-hook", "HOOK.md"),
|
|
||||||
[
|
|
||||||
"---",
|
|
||||||
"name: evil-hook",
|
|
||||||
"description: Evil hook",
|
|
||||||
'metadata: {"openclaw":{"events":["command:new"]}}',
|
|
||||||
"---",
|
|
||||||
"",
|
|
||||||
"# Evil Hook",
|
|
||||||
].join("\n"),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "hooks", "evil-hook", "handler.ts"),
|
|
||||||
"export default async () => {};\n",
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
@@ -242,37 +291,7 @@ describe("installHooksFromArchive", () => {
|
|||||||
const stateDir = makeTempDir();
|
const stateDir = makeTempDir();
|
||||||
const workDir = makeTempDir();
|
const workDir = makeTempDir();
|
||||||
const archivePath = path.join(workDir, "hooks.tar");
|
const archivePath = path.join(workDir, "hooks.tar");
|
||||||
const pkgDir = path.join(workDir, "package");
|
fs.writeFileSync(archivePath, tarReservedIdBuffer);
|
||||||
|
|
||||||
fs.mkdirSync(path.join(pkgDir, "hooks", "reserved-hook"), { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "package.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
name: "@evil/.",
|
|
||||||
version: "0.0.1",
|
|
||||||
openclaw: { hooks: ["./hooks/reserved-hook"] },
|
|
||||||
}),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "hooks", "reserved-hook", "HOOK.md"),
|
|
||||||
[
|
|
||||||
"---",
|
|
||||||
"name: reserved-hook",
|
|
||||||
"description: Reserved hook",
|
|
||||||
'metadata: {"openclaw":{"events":["command:new"]}}',
|
|
||||||
"---",
|
|
||||||
"",
|
|
||||||
"# Reserved Hook",
|
|
||||||
].join("\n"),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "hooks", "reserved-hook", "handler.ts"),
|
|
||||||
"export default async () => {};\n",
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
await tar.c({ cwd: workDir, file: archivePath }, ["package"]);
|
|
||||||
|
|
||||||
const hooksDir = path.join(stateDir, "hooks");
|
const hooksDir = path.join(stateDir, "hooks");
|
||||||
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
const result = await installHooksFromArchive({ archivePath, hooksDir });
|
||||||
@@ -381,37 +400,7 @@ describe("installHooksFromPath", () => {
|
|||||||
|
|
||||||
describe("installHooksFromNpmSpec", () => {
|
describe("installHooksFromNpmSpec", () => {
|
||||||
it("uses --ignore-scripts for npm pack and cleans up temp dir", async () => {
|
it("uses --ignore-scripts for npm pack and cleans up temp dir", async () => {
|
||||||
const workDir = makeTempDir();
|
|
||||||
const stateDir = makeTempDir();
|
const stateDir = makeTempDir();
|
||||||
const pkgDir = path.join(workDir, "package");
|
|
||||||
fs.mkdirSync(path.join(pkgDir, "hooks", "one-hook"), { recursive: true });
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "package.json"),
|
|
||||||
JSON.stringify({
|
|
||||||
name: "@openclaw/test-hooks",
|
|
||||||
version: "0.0.1",
|
|
||||||
openclaw: { hooks: ["./hooks/one-hook"] },
|
|
||||||
}),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "hooks", "one-hook", "HOOK.md"),
|
|
||||||
[
|
|
||||||
"---",
|
|
||||||
"name: one-hook",
|
|
||||||
"description: One hook",
|
|
||||||
'metadata: {"openclaw":{"events":["command:new"]}}',
|
|
||||||
"---",
|
|
||||||
"",
|
|
||||||
"# One Hook",
|
|
||||||
].join("\n"),
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
fs.writeFileSync(
|
|
||||||
path.join(pkgDir, "hooks", "one-hook", "handler.ts"),
|
|
||||||
"export default async () => {};\n",
|
|
||||||
"utf-8",
|
|
||||||
);
|
|
||||||
|
|
||||||
const run = vi.mocked(runCommandWithTimeout);
|
const run = vi.mocked(runCommandWithTimeout);
|
||||||
let packTmpDir = "";
|
let packTmpDir = "";
|
||||||
@@ -419,7 +408,7 @@ describe("installHooksFromNpmSpec", () => {
|
|||||||
run.mockImplementation(async (argv, opts) => {
|
run.mockImplementation(async (argv, opts) => {
|
||||||
if (argv[0] === "npm" && argv[1] === "pack") {
|
if (argv[0] === "npm" && argv[1] === "pack") {
|
||||||
packTmpDir = String(opts?.cwd ?? "");
|
packTmpDir = String(opts?.cwd ?? "");
|
||||||
await packToArchive({ pkgDir, outDir: packTmpDir, outName: packedName });
|
fs.writeFileSync(path.join(packTmpDir, packedName), npmPackHooksBuffer);
|
||||||
return { code: 0, stdout: `${packedName}\n`, stderr: "", signal: null, killed: false };
|
return { code: 0, stdout: `${packedName}\n`, stderr: "", signal: null, killed: false };
|
||||||
}
|
}
|
||||||
throw new Error(`unexpected command: ${argv.join(" ")}`);
|
throw new Error(`unexpected command: ${argv.join(" ")}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user