
























@@ -102,6 +102,7 @@ describe("config plugin validation", () => {
102102let voiceCallSchemaPluginDir = "";
103103let bundlePluginDir = "";
104104let manifestlessClaudeBundleDir = "";
105+let blockedPluginDir = "";
105106const suiteEnv = () =>
106107({
107108HOME: suiteHome,
@@ -188,6 +189,12 @@ describe("config plugin validation", () => {
188189await writeManifestlessClaudeBundleFixture({
189190dir: manifestlessClaudeBundleDir,
190191});
192+blockedPluginDir = path.join(suiteHome, "blocked-plugin");
193+await writePluginFixture({
194+dir: blockedPluginDir,
195+id: "blocked-plugin",
196+schema: { type: "object" },
197+});
191198voiceCallSchemaPluginDir = path.join(suiteHome, "voice-call-schema-plugin");
192199const voiceCallManifestPath = path.join(
193200process.cwd(),
@@ -246,6 +253,165 @@ describe("config plugin validation", () => {
246253}
247254});
248255256+it.runIf(process.platform !== "win32")(
257+"reports configured blocked plugins without stale not-found wording",
258+async () => {
259+await fs.chmod(blockedPluginDir, 0o777);
260+try {
261+const res = validateInSuite({
262+agents: { list: [{ id: "pi" }] },
263+plugins: {
264+enabled: true,
265+load: { paths: [blockedPluginDir] },
266+entries: { "blocked-plugin": { enabled: true } },
267+allow: ["blocked-plugin"],
268+},
269+});
270+271+expect(res.ok).toBe(true);
272+if (!res.ok) {
273+return;
274+}
275+expect(res.warnings).toEqual(
276+expect.arrayContaining([
277+expect.objectContaining({
278+path: "plugins.entries.blocked-plugin",
279+message: expect.stringContaining("plugin present but blocked: blocked-plugin"),
280+}),
281+expect.objectContaining({
282+path: "plugins.allow",
283+message: expect.stringContaining("plugin present but blocked: blocked-plugin"),
284+}),
285+]),
286+);
287+expect(
288+res.warnings.some(
289+(warning) =>
290+warning.message.includes("plugin not found: blocked-plugin") ||
291+warning.message.includes("remove it from plugins config"),
292+),
293+).toBe(false);
294+} finally {
295+await chmodSafeDir(blockedPluginDir);
296+}
297+},
298+);
299+300+it("maps legacy blocked diagnostics without plugin ids to configured load paths", () => {
301+const res = validateConfigObjectWithPlugins(
302+{
303+agents: { list: [{ id: "pi" }] },
304+plugins: {
305+enabled: true,
306+load: { paths: [blockedPluginDir] },
307+entries: { "blocked-plugin": { enabled: true } },
308+allow: ["blocked-plugin"],
309+},
310+},
311+{
312+env: suiteEnv(),
313+pluginMetadataSnapshot: {
314+manifestRegistry: {
315+plugins: [],
316+diagnostics: [
317+{
318+level: "warn",
319+source: path.join(blockedPluginDir, "index.js"),
320+message: `blocked plugin candidate: world-writable path (${blockedPluginDir}, mode=0777)`,
321+},
322+],
323+},
324+},
325+},
326+);
327+328+expect(res.ok).toBe(true);
329+if (!res.ok) {
330+return;
331+}
332+expect(res.warnings).toEqual(
333+expect.arrayContaining([
334+expect.objectContaining({
335+path: "plugins.entries.blocked-plugin",
336+message: expect.stringContaining("plugin present but blocked: blocked-plugin"),
337+}),
338+expect.objectContaining({
339+path: "plugins.allow",
340+message: expect.stringContaining("plugin present but blocked: blocked-plugin"),
341+}),
342+]),
343+);
344+expect(
345+res.warnings.some((warning) => warning.message.includes("plugin not found: blocked-plugin")),
346+).toBe(false);
347+});
348+349+it("does not source-match blocked diagnostics that already name a different plugin id", () => {
350+const aliasDir = path.join(suiteHome, "alias-dir");
351+const res = validateConfigObjectWithPlugins(
352+{
353+agents: { list: [{ id: "pi" }] },
354+plugins: {
355+enabled: true,
356+load: { paths: [aliasDir] },
357+entries: {
358+"actual-id": { enabled: true },
359+"alias-dir": { enabled: true },
360+},
361+allow: ["actual-id", "alias-dir"],
362+},
363+},
364+{
365+env: suiteEnv(),
366+pluginMetadataSnapshot: {
367+manifestRegistry: {
368+plugins: [],
369+diagnostics: [
370+{
371+level: "warn",
372+pluginId: "actual-id",
373+source: path.join(aliasDir, "index.js"),
374+message: `blocked plugin candidate: world-writable path (${aliasDir}, mode=0777)`,
375+},
376+],
377+},
378+},
379+},
380+);
381+382+expect(res.ok).toBe(true);
383+if (!res.ok) {
384+return;
385+}
386+expect(res.warnings).toEqual(
387+expect.arrayContaining([
388+expect.objectContaining({
389+path: "plugins.entries.actual-id",
390+message: expect.stringContaining("plugin present but blocked: actual-id"),
391+}),
392+expect.objectContaining({
393+path: "plugins.allow",
394+message: expect.stringContaining("plugin present but blocked: actual-id"),
395+}),
396+expect.objectContaining({
397+path: "plugins.entries.alias-dir",
398+message:
399+"plugin not found: alias-dir (stale config entry ignored; remove it from plugins config)",
400+}),
401+expect.objectContaining({
402+path: "plugins.allow",
403+message:
404+"plugin not found: alias-dir (stale config entry ignored; remove it from plugins config)",
405+}),
406+]),
407+);
408+expect(
409+res.warnings.some((warning) =>
410+warning.message.includes("plugin present but blocked: alias-dir"),
411+),
412+).toBe(false);
413+});
414+249415it("warns instead of failing for stale channel config backed by missing plugin refs", async () => {
250416const res = validateInSuite({
251417agents: { list: [{ id: "pi" }] },
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。