diff --git a/src/config/config.plugin-validation.test.ts b/src/config/config.plugin-validation.test.ts index 5eec7f2ed..d7e6ae46a 100644 --- a/src/config/config.plugin-validation.test.ts +++ b/src/config/config.plugin-validation.test.ts @@ -5,13 +5,25 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js"; import { validateConfigObjectWithPlugins } from "./config.js"; +async function chmodSafeDir(dir: string) { + if (process.platform === "win32") { + return; + } + await fs.chmod(dir, 0o755); +} + +async function mkdirSafe(dir: string) { + await fs.mkdir(dir, { recursive: true }); + await chmodSafeDir(dir); +} + async function writePluginFixture(params: { dir: string; id: string; schema: Record; channels?: string[]; }) { - await fs.mkdir(params.dir, { recursive: true }); + await mkdirSafe(params.dir); await fs.writeFile( path.join(params.dir, "index.js"), `export default { id: "${params.id}", register() {} };`, @@ -32,6 +44,7 @@ async function writePluginFixture(params: { } describe("config plugin validation", () => { + const previousUmask = process.umask(0o022); let fixtureRoot = ""; let suiteHome = ""; let badPluginDir = ""; @@ -53,8 +66,9 @@ describe("config plugin validation", () => { beforeAll(async () => { fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-plugin-validation-")); + await chmodSafeDir(fixtureRoot); suiteHome = path.join(fixtureRoot, "home"); - await fs.mkdir(suiteHome, { recursive: true }); + await mkdirSafe(suiteHome); badPluginDir = path.join(suiteHome, "bad-plugin"); enumPluginDir = path.join(suiteHome, "enum-plugin"); bluebubblesPluginDir = path.join(suiteHome, "bluebubbles-plugin"); @@ -122,6 +136,7 @@ describe("config plugin validation", () => { afterAll(async () => { await fs.rm(fixtureRoot, { recursive: true, force: true }); clearPluginManifestRegistryCache(); + process.umask(previousUmask); }); it("reports missing plugin refs across load paths, entries, and allowlist surfaces", async () => { diff --git a/src/config/plugin-auto-enable.test.ts b/src/config/plugin-auto-enable.test.ts index fd2ccfa4b..c44a600a2 100644 --- a/src/config/plugin-auto-enable.test.ts +++ b/src/config/plugin-auto-enable.test.ts @@ -1,7 +1,7 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterAll, afterEach, describe, expect, it } from "vitest"; import { clearPluginDiscoveryCache } from "../plugins/discovery.js"; import { clearPluginManifestRegistryCache, @@ -11,15 +11,34 @@ import { validateConfigObject } from "./config.js"; import { applyPluginAutoEnable } from "./plugin-auto-enable.js"; const tempDirs: string[] = []; +const previousUmask = process.umask(0o022); + +function chmodSafeDir(dir: string) { + if (process.platform === "win32") { + return; + } + fs.chmodSync(dir, 0o755); +} + +function mkdtempSafe(prefix: string) { + const dir = fs.mkdtempSync(prefix); + chmodSafeDir(dir); + return dir; +} + +function mkdirSafe(dir: string) { + fs.mkdirSync(dir, { recursive: true }); + chmodSafeDir(dir); +} function makeTempDir() { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-auto-enable-")); + const dir = mkdtempSafe(path.join(os.tmpdir(), "openclaw-plugin-auto-enable-")); tempDirs.push(dir); return dir; } function writePluginManifestFixture(params: { rootDir: string; id: string; channels: string[] }) { - fs.mkdirSync(params.rootDir, { recursive: true }); + mkdirSafe(params.rootDir); fs.writeFileSync( path.join(params.rootDir, "openclaw.plugin.json"), JSON.stringify( @@ -107,6 +126,10 @@ afterEach(() => { } }); +afterAll(() => { + process.umask(previousUmask); +}); + describe("applyPluginAutoEnable", () => { it("auto-enables built-in channels and appends to existing allowlist", () => { const result = applyWithSlackConfig({ plugins: { allow: ["telegram"] } }); @@ -228,7 +251,7 @@ describe("applyPluginAutoEnable", () => { it("uses env-scoped catalog metadata for preferOver auto-enable decisions", () => { const stateDir = makeTempDir(); const catalogPath = path.join(stateDir, "plugins", "catalog.json"); - fs.mkdirSync(path.dirname(catalogPath), { recursive: true }); + mkdirSafe(path.dirname(catalogPath)); fs.writeFileSync( catalogPath, JSON.stringify({ diff --git a/src/plugins/discovery.test.ts b/src/plugins/discovery.test.ts index 39bc1775a..65873cbc2 100644 --- a/src/plugins/discovery.test.ts +++ b/src/plugins/discovery.test.ts @@ -2,14 +2,27 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterAll, afterEach, describe, expect, it } from "vitest"; import { clearPluginDiscoveryCache, discoverOpenClawPlugins } from "./discovery.js"; const tempDirs: string[] = []; +const previousUmask = process.umask(0o022); + +function chmodSafeDir(dir: string) { + if (process.platform === "win32") { + return; + } + fs.chmodSync(dir, 0o755); +} + +function mkdirSafe(dir: string) { + fs.mkdirSync(dir, { recursive: true }); + chmodSafeDir(dir); +} function makeTempDir() { const dir = path.join(os.tmpdir(), `openclaw-plugins-${randomUUID()}`); - fs.mkdirSync(dir, { recursive: true }); + mkdirSafe(dir); tempDirs.push(dir); return dir; } @@ -62,17 +75,21 @@ afterEach(() => { } }); +afterAll(() => { + process.umask(previousUmask); +}); + describe("discoverOpenClawPlugins", () => { it("discovers global and workspace extensions", async () => { const stateDir = makeTempDir(); const workspaceDir = path.join(stateDir, "workspace"); const globalExt = path.join(stateDir, "extensions"); - fs.mkdirSync(globalExt, { recursive: true }); + mkdirSafe(globalExt); fs.writeFileSync(path.join(globalExt, "alpha.ts"), "export default function () {}", "utf-8"); const workspaceExt = path.join(workspaceDir, ".openclaw", "extensions"); - fs.mkdirSync(workspaceExt, { recursive: true }); + mkdirSafe(workspaceExt); fs.writeFileSync(path.join(workspaceExt, "beta.ts"), "export default function () {}", "utf-8"); const { candidates } = await discoverWithStateDir(stateDir, { workspaceDir }); @@ -87,7 +104,7 @@ describe("discoverOpenClawPlugins", () => { const homeDir = makeTempDir(); const workspaceRoot = path.join(homeDir, "workspace"); const workspaceExt = path.join(workspaceRoot, ".openclaw", "extensions"); - fs.mkdirSync(workspaceExt, { recursive: true }); + mkdirSafe(workspaceExt); fs.writeFileSync(path.join(workspaceExt, "tilde-workspace.ts"), "export default {}", "utf-8"); const result = discoverOpenClawPlugins({ @@ -106,22 +123,22 @@ describe("discoverOpenClawPlugins", () => { it("ignores backup and disabled plugin directories in scanned roots", async () => { const stateDir = makeTempDir(); const globalExt = path.join(stateDir, "extensions"); - fs.mkdirSync(globalExt, { recursive: true }); + mkdirSafe(globalExt); const backupDir = path.join(globalExt, "feishu.backup-20260222"); - fs.mkdirSync(backupDir, { recursive: true }); + mkdirSafe(backupDir); fs.writeFileSync(path.join(backupDir, "index.ts"), "export default function () {}", "utf-8"); const disabledDir = path.join(globalExt, "telegram.disabled.20260222"); - fs.mkdirSync(disabledDir, { recursive: true }); + mkdirSafe(disabledDir); fs.writeFileSync(path.join(disabledDir, "index.ts"), "export default function () {}", "utf-8"); const bakDir = path.join(globalExt, "discord.bak"); - fs.mkdirSync(bakDir, { recursive: true }); + mkdirSafe(bakDir); fs.writeFileSync(path.join(bakDir, "index.ts"), "export default function () {}", "utf-8"); const liveDir = path.join(globalExt, "live"); - fs.mkdirSync(liveDir, { recursive: true }); + mkdirSafe(liveDir); fs.writeFileSync(path.join(liveDir, "index.ts"), "export default function () {}", "utf-8"); const { candidates } = await discoverWithStateDir(stateDir, {}); @@ -136,7 +153,7 @@ describe("discoverOpenClawPlugins", () => { it("loads package extension packs", async () => { const stateDir = makeTempDir(); const globalExt = path.join(stateDir, "extensions", "pack"); - fs.mkdirSync(path.join(globalExt, "src"), { recursive: true }); + mkdirSafe(path.join(globalExt, "src")); writePluginPackageManifest({ packageDir: globalExt, @@ -164,7 +181,7 @@ describe("discoverOpenClawPlugins", () => { it("derives unscoped ids for scoped packages", async () => { const stateDir = makeTempDir(); const globalExt = path.join(stateDir, "extensions", "voice-call-pack"); - fs.mkdirSync(path.join(globalExt, "src"), { recursive: true }); + mkdirSafe(path.join(globalExt, "src")); writePluginPackageManifest({ packageDir: globalExt, @@ -186,7 +203,7 @@ describe("discoverOpenClawPlugins", () => { it("treats configured directory paths as plugin packages", async () => { const stateDir = makeTempDir(); const packDir = path.join(stateDir, "packs", "demo-plugin-dir"); - fs.mkdirSync(packDir, { recursive: true }); + mkdirSafe(packDir); writePluginPackageManifest({ packageDir: packDir, @@ -204,7 +221,7 @@ describe("discoverOpenClawPlugins", () => { const stateDir = makeTempDir(); const globalExt = path.join(stateDir, "extensions", "escape-pack"); const outside = path.join(stateDir, "outside.js"); - fs.mkdirSync(globalExt, { recursive: true }); + mkdirSafe(globalExt); writePluginPackageManifest({ packageDir: globalExt, @@ -224,8 +241,8 @@ describe("discoverOpenClawPlugins", () => { const globalExt = path.join(stateDir, "extensions", "pack"); const outsideDir = path.join(stateDir, "outside"); const linkedDir = path.join(globalExt, "linked"); - fs.mkdirSync(globalExt, { recursive: true }); - fs.mkdirSync(outsideDir, { recursive: true }); + mkdirSafe(globalExt); + mkdirSafe(outsideDir); fs.writeFileSync(path.join(outsideDir, "escape.ts"), "export default {}", "utf-8"); try { fs.symlinkSync(outsideDir, linkedDir, process.platform === "win32" ? "junction" : "dir"); @@ -254,8 +271,8 @@ describe("discoverOpenClawPlugins", () => { const outsideDir = path.join(stateDir, "outside"); const outsideFile = path.join(outsideDir, "escape.ts"); const linkedFile = path.join(globalExt, "escape.ts"); - fs.mkdirSync(globalExt, { recursive: true }); - fs.mkdirSync(outsideDir, { recursive: true }); + mkdirSafe(globalExt); + mkdirSafe(outsideDir); fs.writeFileSync(outsideFile, "export default {}", "utf-8"); try { fs.linkSync(outsideFile, linkedFile); @@ -287,8 +304,8 @@ describe("discoverOpenClawPlugins", () => { const outsideDir = path.join(stateDir, "outside"); const outsideManifest = path.join(outsideDir, "package.json"); const linkedManifest = path.join(globalExt, "package.json"); - fs.mkdirSync(globalExt, { recursive: true }); - fs.mkdirSync(outsideDir, { recursive: true }); + mkdirSafe(globalExt); + mkdirSafe(outsideDir); fs.writeFileSync(path.join(globalExt, "entry.ts"), "export default {}", "utf-8"); fs.writeFileSync( outsideManifest, @@ -315,7 +332,7 @@ describe("discoverOpenClawPlugins", () => { it.runIf(process.platform !== "win32")("blocks world-writable plugin paths", async () => { const stateDir = makeTempDir(); const globalExt = path.join(stateDir, "extensions"); - fs.mkdirSync(globalExt, { recursive: true }); + mkdirSafe(globalExt); const pluginPath = path.join(globalExt, "world-open.ts"); fs.writeFileSync(pluginPath, "export default function () {}", "utf-8"); fs.chmodSync(pluginPath, 0o777); @@ -334,7 +351,7 @@ describe("discoverOpenClawPlugins", () => { const stateDir = makeTempDir(); const bundledDir = path.join(stateDir, "bundled"); const packDir = path.join(bundledDir, "demo-pack"); - fs.mkdirSync(packDir, { recursive: true }); + mkdirSafe(packDir); fs.writeFileSync(path.join(packDir, "index.ts"), "export default function () {}", "utf-8"); fs.chmodSync(packDir, 0o777); @@ -362,7 +379,7 @@ describe("discoverOpenClawPlugins", () => { async () => { const stateDir = makeTempDir(); const globalExt = path.join(stateDir, "extensions"); - fs.mkdirSync(globalExt, { recursive: true }); + mkdirSafe(globalExt); fs.writeFileSync( path.join(globalExt, "owner-mismatch.ts"), "export default function () {}", @@ -382,7 +399,7 @@ describe("discoverOpenClawPlugins", () => { it("reuses discovery results from cache until cleared", async () => { const stateDir = makeTempDir(); const globalExt = path.join(stateDir, "extensions"); - fs.mkdirSync(globalExt, { recursive: true }); + mkdirSafe(globalExt); const pluginPath = path.join(globalExt, "cached.ts"); fs.writeFileSync(pluginPath, "export default function () {}", "utf-8"); @@ -420,8 +437,8 @@ describe("discoverOpenClawPlugins", () => { const stateDirB = makeTempDir(); const globalExtA = path.join(stateDirA, "extensions"); const globalExtB = path.join(stateDirB, "extensions"); - fs.mkdirSync(globalExtA, { recursive: true }); - fs.mkdirSync(globalExtB, { recursive: true }); + mkdirSafe(globalExtA); + mkdirSafe(globalExtB); fs.writeFileSync(path.join(globalExtA, "alpha.ts"), "export default function () {}", "utf-8"); fs.writeFileSync(path.join(globalExtB, "beta.ts"), "export default function () {}", "utf-8"); @@ -450,8 +467,8 @@ describe("discoverOpenClawPlugins", () => { const homeB = makeTempDir(); const pluginA = path.join(homeA, "plugins", "demo.ts"); const pluginB = path.join(homeB, "plugins", "demo.ts"); - fs.mkdirSync(path.dirname(pluginA), { recursive: true }); - fs.mkdirSync(path.dirname(pluginB), { recursive: true }); + mkdirSafe(path.dirname(pluginA)); + mkdirSafe(path.dirname(pluginB)); fs.writeFileSync(pluginA, "export default {}", "utf-8"); fs.writeFileSync(pluginB, "export default {}", "utf-8"); @@ -482,7 +499,7 @@ describe("discoverOpenClawPlugins", () => { const stateDir = makeTempDir(); const pluginA = path.join(stateDir, "plugins", "alpha.ts"); const pluginB = path.join(stateDir, "plugins", "beta.ts"); - fs.mkdirSync(path.dirname(pluginA), { recursive: true }); + mkdirSafe(path.dirname(pluginA)); fs.writeFileSync(pluginA, "export default {}", "utf-8"); fs.writeFileSync(pluginB, "export default {}", "utf-8"); diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 884c81989..95b790b69 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -34,10 +34,29 @@ const { loadOpenClawPlugins, resetGlobalHookRunner, } = await importFreshPluginTestModules(); +const previousUmask = process.umask(0o022); type TempPlugin = { dir: string; file: string; id: string }; -const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-")); +function chmodSafeDir(dir: string) { + if (process.platform === "win32") { + return; + } + fs.chmodSync(dir, 0o755); +} + +function mkdtempSafe(prefix: string) { + const dir = fs.mkdtempSync(prefix); + chmodSafeDir(dir); + return dir; +} + +function mkdirSafe(dir: string) { + fs.mkdirSync(dir, { recursive: true }); + chmodSafeDir(dir); +} + +const fixtureRoot = mkdtempSafe(path.join(os.tmpdir(), "openclaw-plugin-")); let tempDirIndex = 0; const prevBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR; const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} }; @@ -69,7 +88,7 @@ const BUNDLED_TELEGRAM_PLUGIN_BODY = `module.exports = { function makeTempDir() { const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`); - fs.mkdirSync(dir, { recursive: true }); + mkdirSafe(dir); return dir; } @@ -81,7 +100,7 @@ function writePlugin(params: { }): TempPlugin { const dir = params.dir ?? makeTempDir(); const filename = params.filename ?? `${params.id}.cjs`; - fs.mkdirSync(dir, { recursive: true }); + mkdirSafe(dir); const file = path.join(dir, filename); fs.writeFileSync(file, params.body, "utf-8"); fs.writeFileSync( @@ -126,7 +145,7 @@ function loadBundledMemoryPluginRegistry(options?: { if (options?.packageMeta) { pluginDir = path.join(bundledDir, "memory-core"); pluginFilename = options.pluginFilename ?? "index.js"; - fs.mkdirSync(pluginDir, { recursive: true }); + mkdirSafe(pluginDir); fs.writeFileSync( path.join(pluginDir, "package.json"), JSON.stringify( @@ -259,8 +278,8 @@ function createPluginSdkAliasFixture(params?: { const root = makeTempDir(); const srcFile = path.join(root, "src", "plugin-sdk", params?.srcFile ?? "index.ts"); const distFile = path.join(root, "dist", "plugin-sdk", params?.distFile ?? "index.js"); - fs.mkdirSync(path.dirname(srcFile), { recursive: true }); - fs.mkdirSync(path.dirname(distFile), { recursive: true }); + mkdirSafe(path.dirname(srcFile)); + mkdirSafe(path.dirname(distFile)); fs.writeFileSync(srcFile, params?.srcBody ?? "export {};\n", "utf-8"); fs.writeFileSync(distFile, params?.distBody ?? "export {};\n", "utf-8"); return { root, srcFile, distFile }; @@ -281,6 +300,7 @@ afterAll(() => { } catch { // ignore cleanup failures } finally { + process.umask(previousUmask); cachedBundledTelegramDir = ""; cachedBundledMemoryDir = ""; } @@ -571,7 +591,7 @@ describe("loadOpenClawPlugins", () => { const ignoredHome = makeTempDir(); const stateDir = makeTempDir(); const pluginDir = path.join(openclawHome, "plugins", "tracked-install-cache"); - fs.mkdirSync(pluginDir, { recursive: true }); + mkdirSafe(pluginDir); const plugin = writePlugin({ id: "tracked-install-cache", dir: pluginDir, @@ -1271,8 +1291,8 @@ describe("loadOpenClawPlugins", () => { const bundledDir = makeTempDir(); const memoryADir = path.join(bundledDir, "memory-a"); const memoryBDir = path.join(bundledDir, "memory-b"); - fs.mkdirSync(memoryADir, { recursive: true }); - fs.mkdirSync(memoryBDir, { recursive: true }); + mkdirSafe(memoryADir); + mkdirSafe(memoryBDir); writePlugin({ id: "memory-a", dir: memoryADir, @@ -1402,7 +1422,7 @@ describe("loadOpenClawPlugins", () => { const stateDir = makeTempDir(); withEnv({ OPENCLAW_STATE_DIR: stateDir, CLAWDBOT_STATE_DIR: undefined }, () => { const globalDir = path.join(stateDir, "extensions", "feishu"); - fs.mkdirSync(globalDir, { recursive: true }); + mkdirSafe(globalDir); writePlugin({ id: "feishu", body: `module.exports = { id: "feishu", register() {} };`, @@ -1456,7 +1476,7 @@ describe("loadOpenClawPlugins", () => { useNoBundledPlugins(); const workspaceDir = makeTempDir(); const workspaceExtDir = path.join(workspaceDir, ".openclaw", "extensions", "workspace-helper"); - fs.mkdirSync(workspaceExtDir, { recursive: true }); + mkdirSafe(workspaceExtDir); writePlugin({ id: "workspace-helper", body: `module.exports = { id: "workspace-helper", register() {} };`, @@ -1484,7 +1504,7 @@ describe("loadOpenClawPlugins", () => { useNoBundledPlugins(); const workspaceDir = makeTempDir(); const workspaceExtDir = path.join(workspaceDir, ".openclaw", "extensions", "workspace-helper"); - fs.mkdirSync(workspaceExtDir, { recursive: true }); + mkdirSafe(workspaceExtDir); writePlugin({ id: "workspace-helper", body: `module.exports = { id: "workspace-helper", register() {} };`, @@ -1513,7 +1533,7 @@ describe("loadOpenClawPlugins", () => { const stateDir = makeTempDir(); withEnv({ OPENCLAW_STATE_DIR: stateDir, CLAWDBOT_STATE_DIR: undefined }, () => { const globalDir = path.join(stateDir, "extensions", "rogue"); - fs.mkdirSync(globalDir, { recursive: true }); + mkdirSafe(globalDir); writePlugin({ id: "rogue", body: `module.exports = { id: "rogue", register() {} };`, @@ -1549,7 +1569,7 @@ describe("loadOpenClawPlugins", () => { const ignoredHome = makeTempDir(); const stateDir = makeTempDir(); const pluginDir = path.join(openclawHome, "plugins", "tracked-load-path"); - fs.mkdirSync(pluginDir, { recursive: true }); + mkdirSafe(pluginDir); const plugin = writePlugin({ id: "tracked-load-path", dir: pluginDir, @@ -1591,7 +1611,7 @@ describe("loadOpenClawPlugins", () => { const ignoredHome = makeTempDir(); const stateDir = makeTempDir(); const pluginDir = path.join(openclawHome, "plugins", "tracked-install-path"); - fs.mkdirSync(pluginDir, { recursive: true }); + mkdirSafe(pluginDir); const plugin = writePlugin({ id: "tracked-install-path", dir: pluginDir, @@ -1702,7 +1722,7 @@ describe("loadOpenClawPlugins", () => { } const bundledDir = makeTempDir(); const pluginDir = path.join(bundledDir, "hardlinked-bundled"); - fs.mkdirSync(pluginDir, { recursive: true }); + mkdirSafe(pluginDir); const outsideDir = makeTempDir(); const outsideEntry = path.join(outsideDir, "outside.cjs"); diff --git a/src/plugins/manifest-registry.test.ts b/src/plugins/manifest-registry.test.ts index e84158b3c..bbf65d14e 100644 --- a/src/plugins/manifest-registry.test.ts +++ b/src/plugins/manifest-registry.test.ts @@ -2,7 +2,7 @@ import { randomUUID } from "node:crypto"; import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterAll, afterEach, describe, expect, it } from "vitest"; import type { PluginCandidate } from "./discovery.js"; import { clearPluginManifestRegistryCache, @@ -10,10 +10,23 @@ import { } from "./manifest-registry.js"; const tempDirs: string[] = []; +const previousUmask = process.umask(0o022); + +function chmodSafeDir(dir: string) { + if (process.platform === "win32") { + return; + } + fs.chmodSync(dir, 0o755); +} + +function mkdirSafe(dir: string) { + fs.mkdirSync(dir, { recursive: true }); + chmodSafeDir(dir); +} function makeTempDir() { const dir = path.join(os.tmpdir(), `openclaw-manifest-registry-${randomUUID()}`); - fs.mkdirSync(dir, { recursive: true }); + mkdirSafe(dir); tempDirs.push(dir); return dir; } @@ -133,6 +146,10 @@ afterEach(() => { } }); +afterAll(() => { + process.umask(previousUmask); +}); + describe("loadPluginManifestRegistry", () => { it("emits duplicate warning for truly distinct plugins with same id", () => { const dirA = makeTempDir(); @@ -214,7 +231,7 @@ describe("loadPluginManifestRegistry", () => { it("prefers higher-precedence origins for the same physical directory (config > workspace > global > bundled)", () => { const dir = makeTempDir(); - fs.mkdirSync(path.join(dir, "sub"), { recursive: true }); + mkdirSafe(path.join(dir, "sub")); const manifest = { id: "precedence-plugin", configSchema: { type: "object" } }; writeManifest(dir, manifest); @@ -274,8 +291,8 @@ describe("loadPluginManifestRegistry", () => { const bundledB = makeTempDir(); const matrixA = path.join(bundledA, "matrix"); const matrixB = path.join(bundledB, "matrix"); - fs.mkdirSync(matrixA, { recursive: true }); - fs.mkdirSync(matrixB, { recursive: true }); + mkdirSafe(matrixA); + mkdirSafe(matrixB); writeManifest(matrixA, { id: "matrix", name: "Matrix A", @@ -317,8 +334,8 @@ describe("loadPluginManifestRegistry", () => { const homeB = makeTempDir(); const demoA = path.join(homeA, "plugins", "demo"); const demoB = path.join(homeB, "plugins", "demo"); - fs.mkdirSync(demoA, { recursive: true }); - fs.mkdirSync(demoB, { recursive: true }); + mkdirSafe(demoA); + mkdirSafe(demoB); writeManifest(demoA, { id: "demo", name: "Demo A",