diff --git a/src/gateway/server.cron.test.ts b/src/gateway/server.cron.test.ts index 0bdefeb6a04..74979352ba0 100644 --- a/src/gateway/server.cron.test.ts +++ b/src/gateway/server.cron.test.ts @@ -36,6 +36,7 @@ vi.mock("../infra/net/fetch-guard.js", () => ({ installGatewayTestHooks({ scope: "suite" }); const CRON_WAIT_INTERVAL_MS = 5; const CRON_WAIT_TIMEOUT_MS = 3_000; +const EMPTY_CRON_STORE_CONTENT = JSON.stringify({ version: 1, jobs: [] }); let cronSuiteTempRootPromise: Promise | null = null; let cronSuiteCaseId = 0; @@ -79,10 +80,20 @@ async function waitForCondition(check: () => boolean | Promise, timeout ); } +async function createCronCasePaths(tempPrefix: string): Promise<{ + dir: string; + storePath: string; +}> { + const suiteRoot = await getCronSuiteTempRoot(); + const dir = path.join(suiteRoot, `${tempPrefix}${cronSuiteCaseId++}`); + const storePath = path.join(dir, "cron", "jobs.json"); + await fs.mkdir(path.dirname(storePath), { recursive: true }); + return { dir, storePath }; +} + async function cleanupCronTestRun(params: { ws: { close: () => void }; server: { close: () => Promise }; - dir: string; prevSkipCron: string | undefined; clearSessionConfig?: boolean; }) { @@ -108,16 +119,13 @@ async function setupCronTestRun(params: { }): Promise<{ prevSkipCron: string | undefined; dir: string }> { const prevSkipCron = process.env.OPENCLAW_SKIP_CRON; process.env.OPENCLAW_SKIP_CRON = "0"; - const suiteRoot = await getCronSuiteTempRoot(); - const dir = path.join(suiteRoot, `${params.tempPrefix}${cronSuiteCaseId++}`); - await fs.mkdir(dir, { recursive: true }); - testState.cronStorePath = path.join(dir, "cron", "jobs.json"); + const { dir, storePath } = await createCronCasePaths(params.tempPrefix); + testState.cronStorePath = storePath; testState.sessionConfig = params.sessionConfig; testState.cronEnabled = params.cronEnabled; - await fs.mkdir(path.dirname(testState.cronStorePath), { recursive: true }); await fs.writeFile( testState.cronStorePath, - JSON.stringify({ version: 1, jobs: params.jobs ?? [] }), + params.jobs ? JSON.stringify({ version: 1, jobs: params.jobs }) : EMPTY_CRON_STORE_CONTENT, ); return { prevSkipCron, dir }; } @@ -138,7 +146,7 @@ describe("gateway server cron", () => { }); test("handles cron CRUD, normalization, and patch semantics", { timeout: 20_000 }, async () => { - const { prevSkipCron, dir } = await setupCronTestRun({ + const { prevSkipCron } = await setupCronTestRun({ tempPrefix: "openclaw-gw-cron-", sessionConfig: { mainKey: "primary" }, cronEnabled: false, @@ -403,7 +411,6 @@ describe("gateway server cron", () => { await cleanupCronTestRun({ ws, server, - dir, prevSkipCron, clearSessionConfig: true, }); @@ -514,7 +521,7 @@ describe("gateway server cron", () => { const runs = autoEntries?.entries ?? []; expect(runs.at(-1)?.jobId).toBe(autoJobId); } finally { - await cleanupCronTestRun({ ws, server, dir, prevSkipCron }); + await cleanupCronTestRun({ ws, server, prevSkipCron }); } }, 45_000); @@ -532,7 +539,7 @@ describe("gateway server cron", () => { payload: { kind: "systemEvent", text: "legacy webhook" }, state: {}, }; - const { prevSkipCron, dir } = await setupCronTestRun({ + const { prevSkipCron } = await setupCronTestRun({ tempPrefix: "openclaw-gw-cron-webhook-", cronEnabled: false, jobs: [legacyNotifyJob], @@ -741,7 +748,7 @@ describe("gateway server cron", () => { await yieldToEventLoop(); expect(fetchWithSsrFGuardMock).toHaveBeenCalledTimes(1); } finally { - await cleanupCronTestRun({ ws, server, dir, prevSkipCron }); + await cleanupCronTestRun({ ws, server, prevSkipCron }); } }, 60_000); }); diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index 113483f87b7..e18362e18e5 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -29,6 +29,34 @@ const archiveFixturePathCache = new Map(); const dynamicArchiveTemplatePathCache = new Map(); let installPluginFromDirTemplateDir = ""; let manifestInstallTemplateDir = ""; +const DYNAMIC_ARCHIVE_TEMPLATE_PRESETS = [ + { + outName: "traversal.tgz", + withDistIndex: true, + packageJson: { + name: "@evil/..", + version: "0.0.1", + openclaw: { extensions: ["./dist/index.js"] }, + } as Record, + }, + { + outName: "reserved.tgz", + withDistIndex: true, + packageJson: { + name: "@evil/.", + version: "0.0.1", + openclaw: { extensions: ["./dist/index.js"] }, + } as Record, + }, + { + outName: "bad.tgz", + withDistIndex: false, + packageJson: { + name: "@openclaw/nope", + version: "0.0.1", + } as Record, + }, +]; function ensureSuiteTempRoot() { if (suiteTempRoot) { @@ -41,7 +69,7 @@ function ensureSuiteTempRoot() { function makeTempDir() { const dir = path.join(ensureSuiteTempRoot(), `case-${String(tempDirCounter)}`); tempDirCounter += 1; - fs.mkdirSync(dir, { recursive: true }); + fs.mkdirSync(dir); return dir; } @@ -157,8 +185,10 @@ function setupPluginInstallDirs() { } function setupInstallPluginFromDirFixture(params?: { devDependencies?: Record }) { - const stateDir = makeTempDir(); - const pluginDir = path.join(makeTempDir(), "plugin"); + const caseDir = makeTempDir(); + const stateDir = path.join(caseDir, "state"); + const pluginDir = path.join(caseDir, "plugin"); + fs.mkdirSync(stateDir, { recursive: true }); fs.cpSync(installPluginFromDirTemplateDir, pluginDir, { recursive: true }); if (params?.devDependencies) { const packageJsonPath = path.join(pluginDir, "package.json"); @@ -185,8 +215,10 @@ async function installFromDirWithWarnings(params: { pluginDir: string; extension } function setupManifestInstallFixture(params: { manifestId: string }) { - const stateDir = makeTempDir(); - const pluginDir = path.join(makeTempDir(), "plugin-src"); + const caseDir = makeTempDir(); + const stateDir = path.join(caseDir, "state"); + const pluginDir = path.join(caseDir, "plugin-src"); + fs.mkdirSync(stateDir, { recursive: true }); fs.cpSync(manifestInstallTemplateDir, pluginDir, { recursive: true }); fs.writeFileSync( path.join(pluginDir, "openclaw.plugin.json"), @@ -226,31 +258,11 @@ async function installArchivePackageAndReturnResult(params: { withDistIndex?: boolean; }) { const stateDir = makeTempDir(); - const templateKey = JSON.stringify({ + const archivePath = await ensureDynamicArchiveTemplate({ + outName: params.outName, 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({ @@ -260,6 +272,46 @@ async function installArchivePackageAndReturnResult(params: { return result; } +function buildDynamicArchiveTemplateKey(params: { + packageJson: Record; + withDistIndex: boolean; +}): string { + return JSON.stringify({ + packageJson: params.packageJson, + withDistIndex: params.withDistIndex, + }); +} + +async function ensureDynamicArchiveTemplate(params: { + packageJson: Record; + outName: string; + withDistIndex: boolean; +}): Promise { + const templateKey = buildDynamicArchiveTemplateKey({ + packageJson: params.packageJson, + withDistIndex: params.withDistIndex, + }); + const cachedPath = dynamicArchiveTemplatePathCache.get(templateKey); + if (cachedPath) { + return cachedPath; + } + 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"); + const archivePath = await packToArchive({ + pkgDir, + outDir: ensureSuiteFixtureRoot(), + outName: params.outName, + }); + dynamicArchiveTemplatePathCache.set(templateKey, archivePath); + return archivePath; +} + afterAll(() => { if (!suiteTempRoot) { return; @@ -327,6 +379,14 @@ beforeAll(async () => { }), "utf-8", ); + + for (const preset of DYNAMIC_ARCHIVE_TEMPLATE_PRESETS) { + await ensureDynamicArchiveTemplate({ + packageJson: preset.packageJson, + outName: preset.outName, + withDistIndex: preset.withDistIndex, + }); + } }); beforeEach(() => {