From 4cb33d48cddb527101123e3ff69f73a00fd7e049 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 9 Aug 2026 04:35:00 -0700 Subject: [PATCH] refactor(config): consolidate regression fixtures (#114459) --- src/config/group-scope-tree.test.ts | 19 +++ .../plugin-auto-enable.channels.test.ts | 124 +++++------------ .../sessions/archive-compression.test.ts | 26 ++++ src/config/sessions/delivery-info.test.ts | 86 +++--------- src/config/sessions/store.pruning.test.ts | 130 ++++++------------ .../validation.channel-metadata.test.ts | 92 ++++--------- 6 files changed, 160 insertions(+), 317 deletions(-) diff --git a/src/config/group-scope-tree.test.ts b/src/config/group-scope-tree.test.ts index 30230b196e94..4c44e57dd11b 100644 --- a/src/config/group-scope-tree.test.ts +++ b/src/config/group-scope-tree.test.ts @@ -3,12 +3,31 @@ import { describe, expect, it } from "vitest"; import type { OpenClawConfig } from "./config.js"; import { resolveChannelGroupRequireMention, resolveToolsBySender } from "./group-policy.js"; import { + resolveScopeKeyCaseInsensitive, resolveScopeIntroHint, resolveScopeRequireMention, resolveScopeToolsPolicy, type ScopeTree, } from "./group-scope-tree.js"; +describe("resolveScopeKeyCaseInsensitive", () => { + it("preserves exact scope identity before case-insensitive fallback", () => { + const tree: ScopeTree = { + scopes: { + "Room:Mixed": { requireMention: true }, + "room:mixed": { requireMention: false }, + }, + }; + + expect(resolveScopeKeyCaseInsensitive(tree, "Room:Mixed")).toBe("Room:Mixed"); + expect(resolveScopeKeyCaseInsensitive(tree, "ROOM:MIXED")).toBe("Room:Mixed"); + expect(resolveScopeKeyCaseInsensitive(tree, " room:mixed ")).toBe("Room:Mixed"); + expect(resolveScopeKeyCaseInsensitive(tree, "unknown")).toBeUndefined(); + expect(resolveScopeKeyCaseInsensitive(tree, undefined)).toBeUndefined(); + expect(resolveScopeKeyCaseInsensitive(tree, null)).toBeUndefined(); + }); +}); + describe("resolveScopeRequireMention", () => { const scalarCases: Array<{ name: string; diff --git a/src/config/plugin-auto-enable.channels.test.ts b/src/config/plugin-auto-enable.channels.test.ts index a45a27d86c47..754f0caab628 100644 --- a/src/config/plugin-auto-enable.channels.test.ts +++ b/src/config/plugin-auto-enable.channels.test.ts @@ -34,6 +34,30 @@ function applyWithApnChannelConfig(extra?: { }); } +function materializeEnvCatalogCandidates( + stateDir: string, + candidates: Parameters[0]["candidates"] = [ + { pluginId: "env-primary", kind: "channel-configured", channelId: "env-primary" }, + { pluginId: "env-secondary", kind: "channel-configured", channelId: "env-secondary" }, + ], +) { + return materializePluginAutoEnableCandidates({ + config: { + channels: { + "env-primary": { token: "primary" }, + "env-secondary": { token: "secondary" }, + }, + }, + candidates, + env: { + ...makeIsolatedEnv(), + OPENCLAW_STATE_DIR: stateDir, + OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", + }, + manifestRegistry: makeRegistry([]), + }); +} + beforeEach(() => { resetPluginAutoEnableTestState(); }); @@ -73,32 +97,7 @@ describe("applyPluginAutoEnable channels", () => { "utf-8", ); - const result = materializePluginAutoEnableCandidates({ - config: { - channels: { - "env-primary": { token: "primary" }, - "env-secondary": { token: "secondary" }, - }, - }, - candidates: [ - { - pluginId: "env-primary", - kind: "channel-configured", - channelId: "env-primary", - }, - { - pluginId: "env-secondary", - kind: "channel-configured", - channelId: "env-secondary", - }, - ], - env: { - ...makeIsolatedEnv(), - OPENCLAW_STATE_DIR: stateDir, - OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", - }, - manifestRegistry: makeRegistry([]), - }); + const result = materializeEnvCatalogCandidates(stateDir); expect(result.config.plugins?.entries?.["env-secondary"]?.enabled).toBe(true); expect(result.config.plugins?.entries?.["env-primary"]).toBeUndefined(); @@ -151,25 +150,14 @@ describe("applyPluginAutoEnable channels", () => { const realpathSpy = vi.spyOn(fs, "realpathSync"); try { - materializePluginAutoEnableCandidates({ - config: { - channels: { - "env-primary": { token: "primary" }, - "env-secondary": { token: "secondary" }, - }, - }, - candidates: Array.from({ length: 20 }, (_, index) => ({ + materializeEnvCatalogCandidates( + stateDir, + Array.from({ length: 20 }, (_, index) => ({ pluginId: index % 2 === 0 ? "env-primary" : "env-secondary", kind: "channel-configured" as const, channelId: index % 2 === 0 ? "env-primary" : "env-secondary", })), - env: { - ...makeIsolatedEnv(), - OPENCLAW_STATE_DIR: stateDir, - OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", - }, - manifestRegistry: makeRegistry([]), - }); + ); expect( realpathSpy.mock.calls.filter(([filePath]) => @@ -211,32 +199,7 @@ describe("applyPluginAutoEnable channels", () => { const catalogPath = path.join(pluginsDir, "catalog.json"); fs.symlinkSync(realPath, catalogPath); - const result = materializePluginAutoEnableCandidates({ - config: { - channels: { - "env-primary": { token: "primary" }, - "env-secondary": { token: "secondary" }, - }, - }, - candidates: [ - { - pluginId: "env-primary", - kind: "channel-configured" as const, - channelId: "env-primary", - }, - { - pluginId: "env-secondary", - kind: "channel-configured" as const, - channelId: "env-secondary", - }, - ], - env: { - ...makeIsolatedEnv(), - OPENCLAW_STATE_DIR: stateDir, - OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", - }, - manifestRegistry: makeRegistry([]), - }); + const result = materializeEnvCatalogCandidates(stateDir); expect(result.config.plugins?.entries?.["env-secondary"]?.enabled).toBe(true); expect(result.config.plugins?.entries?.["env-primary"]).toBeUndefined(); @@ -257,32 +220,7 @@ describe("applyPluginAutoEnable channels", () => { fs.closeSync(fd); } - const result = materializePluginAutoEnableCandidates({ - config: { - channels: { - "env-primary": { token: "primary" }, - "env-secondary": { token: "secondary" }, - }, - }, - candidates: [ - { - pluginId: "env-primary", - kind: "channel-configured" as const, - channelId: "env-primary", - }, - { - pluginId: "env-secondary", - kind: "channel-configured" as const, - channelId: "env-secondary", - }, - ], - env: { - ...makeIsolatedEnv(), - OPENCLAW_STATE_DIR: stateDir, - OPENCLAW_BUNDLED_PLUGINS_DIR: "/nonexistent/bundled/plugins", - }, - manifestRegistry: makeRegistry([]), - }); + const result = materializeEnvCatalogCandidates(stateDir); // Selection continues: env-secondary is still auto-enabled. expect(result.config.plugins?.entries?.["env-secondary"]?.enabled).toBe(true); diff --git a/src/config/sessions/archive-compression.test.ts b/src/config/sessions/archive-compression.test.ts index 0bded3569f4f..8c8c719dbad5 100644 --- a/src/config/sessions/archive-compression.test.ts +++ b/src/config/sessions/archive-compression.test.ts @@ -4,6 +4,7 @@ import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { + decodeSessionArchiveBytes, encodeSessionArchiveContent, materializeSessionArchiveForRead, readSessionArchiveContentSync, @@ -30,6 +31,31 @@ function makeTempDir(): string { } describe("archive compression", () => { + it("decodes both plain bytes and runtime-supported compressed archive bytes", () => { + const content = `${JSON.stringify({ type: "message", body: "archive round trip" })}\n`; + const encoded = encodeSessionArchiveContent(content); + + expect(decodeSessionArchiveBytes(Buffer.from(content, "utf8"), false)).toBe(content); + expect(decodeSessionArchiveBytes(encoded.bytes, encoded.suffix !== "")).toBe(content); + }); + + it("invalidates a materialized cache when its source archive is removed", () => { + const encoded = encodeSessionArchiveContent("cached archive contents\n"); + if (encoded.suffix !== SESSION_ARCHIVE_ZSTD_SUFFIX) { + return; + } + const dir = makeTempDir(); + const archivePath = path.join(dir, `removed.jsonl.deleted.2026-07-11${encoded.suffix}`); + fs.writeFileSync(archivePath, encoded.bytes); + const cachePath = materializeSessionArchiveForRead(archivePath); + expect(fs.existsSync(cachePath)).toBe(true); + + fs.rmSync(archivePath); + + expect(() => materializeSessionArchiveForRead(archivePath)).toThrow(); + expect(fs.existsSync(cachePath)).toBe(false); + }); + it("round-trips archived transcript content through encode and read", () => { const content = `${JSON.stringify({ type: "message", body: "hello" })}\n`.repeat(200); const encoded = encodeSessionArchiveContent(content); diff --git a/src/config/sessions/delivery-info.test.ts b/src/config/sessions/delivery-info.test.ts index 1e6b2039c23e..64012d449dcb 100644 --- a/src/config/sessions/delivery-info.test.ts +++ b/src/config/sessions/delivery-info.test.ts @@ -71,6 +71,14 @@ const buildEntry = (deliveryContext: DeliveryContext): SessionEntryFixture => ({ deliveryContext, }); +function createMixedCaseMatrixDelivery(): DeliveryContext { + return { channel: "matrix", to: "room:!MixedCase:Example.Org", accountId: "matrix-account" }; +} + +function createTelegramUserDelivery(): DeliveryContext { + return { channel: "telegram", to: "telegram:user-123", accountId: "default" }; +} + beforeAll(async () => { ({ extractDeliveryInfo } = await import("./delivery-info.js")); }); @@ -121,11 +129,7 @@ describe("extractDeliveryInfo", () => { it("reads borrowed accessor views for direct session keys", () => { const sessionKey = "agent:main:telegram:dm:user-123"; - storeState.store[sessionKey] = buildEntry({ - channel: "telegram", - to: "telegram:user-123", - accountId: "default", - }); + storeState.store[sessionKey] = buildEntry(createTelegramUserDelivery()); const result = extractDeliveryInfo(sessionKey); @@ -142,11 +146,7 @@ describe("extractDeliveryInfo", () => { // extractDeliveryInfo would return no delivery context. storeState.store = new Proxy( { - [sessionKey]: buildEntry({ - channel: "telegram", - to: "telegram:user-123", - accountId: "default", - }), + [sessionKey]: buildEntry(createTelegramUserDelivery()), }, { ownKeys() { @@ -158,31 +158,19 @@ describe("extractDeliveryInfo", () => { const result = extractDeliveryInfo(sessionKey); expect(result).toEqual({ - deliveryContext: { - channel: "telegram", - to: "telegram:user-123", - accountId: "default", - }, + deliveryContext: createTelegramUserDelivery(), threadId: undefined, }); }); it("returns deliveryContext for direct session keys", () => { const sessionKey = "agent:main:telegram:dm:user-123"; - storeState.store[sessionKey] = buildEntry({ - channel: "telegram", - to: "telegram:user-123", - accountId: "default", - }); + storeState.store[sessionKey] = buildEntry(createTelegramUserDelivery()); const result = extractDeliveryInfo(sessionKey); expect(result).toEqual({ - deliveryContext: { - channel: "telegram", - to: "telegram:user-123", - accountId: "default", - }, + deliveryContext: createTelegramUserDelivery(), threadId: undefined, }); }); @@ -366,11 +354,7 @@ describe("extractDeliveryInfo", () => { storeState.store[sessionKey] = { sessionId: "direct-routable-session", updatedAt: Date.now() - 1_000, - deliveryContext: { - channel: "matrix", - to: "room:!MixedCase:Example.Org", - accountId: "matrix-account", - }, + deliveryContext: createMixedCaseMatrixDelivery(), }; storeState.store[canonicalKey] = { sessionId: "fresh-normalized-session", @@ -383,11 +367,7 @@ describe("extractDeliveryInfo", () => { const result = extractDeliveryInfo(sessionKey); expect(result).toEqual({ - deliveryContext: { - channel: "matrix", - to: "room:!MixedCase:Example.Org", - accountId: "matrix-account", - }, + deliveryContext: createMixedCaseMatrixDelivery(), threadId: undefined, }); }); @@ -492,11 +472,7 @@ describe("extractDeliveryInfo", () => { storeState.store[queriedKey] = { sessionId: "exact-mixedcase-session", updatedAt: Date.now() - 1_000, - deliveryContext: { - channel: "matrix", - to: "room:!MixedCase:Example.Org", - accountId: "matrix-account", - }, + deliveryContext: createMixedCaseMatrixDelivery(), }; storeState.store[legacyFoldedKey] = { sessionId: "fresher-legacy-folded-session", @@ -511,11 +487,7 @@ describe("extractDeliveryInfo", () => { const result = extractDeliveryInfo(queriedKey); expect(result).toEqual({ - deliveryContext: { - channel: "matrix", - to: "room:!MixedCase:Example.Org", - accountId: "matrix-account", - }, + deliveryContext: createMixedCaseMatrixDelivery(), threadId: undefined, }); }); @@ -569,11 +541,7 @@ describe("extractDeliveryInfo", () => { it("does not return a mixed-case Matrix sibling for a lowercase room query", () => { const queriedKey = "agent:main:matrix:channel:!mixedcase:example.org"; const mixedSiblingKey = "agent:main:matrix:channel:!MixedCase:Example.Org"; - storeState.store[mixedSiblingKey] = buildEntry({ - channel: "matrix", - to: "room:!MixedCase:Example.Org", - accountId: "matrix-account", - }); + storeState.store[mixedSiblingKey] = buildEntry(createMixedCaseMatrixDelivery()); const result = extractDeliveryInfo(queriedKey); @@ -585,11 +553,7 @@ describe("extractDeliveryInfo", () => { it("does not return an exact lowercase Matrix key with mixed-case delivery metadata", () => { const queriedKey = "agent:main:matrix:channel:!mixedcase:example.org"; - storeState.store[queriedKey] = buildEntry({ - channel: "matrix", - to: "room:!MixedCase:Example.Org", - accountId: "matrix-account", - }); + storeState.store[queriedKey] = buildEntry(createMixedCaseMatrixDelivery()); const result = extractDeliveryInfo(queriedKey); @@ -602,20 +566,12 @@ describe("extractDeliveryInfo", () => { it("returns a confirmed lowercased Matrix legacy artifact for a mixed-case key", () => { const queriedKey = "agent:main:matrix:channel:!MixedCase:Example.Org"; const legacyArtifactKey = "agent:main:matrix:channel:!mixedcase:example.org"; - storeState.store[legacyArtifactKey] = buildEntry({ - channel: "matrix", - to: "room:!MixedCase:Example.Org", - accountId: "matrix-account", - }); + storeState.store[legacyArtifactKey] = buildEntry(createMixedCaseMatrixDelivery()); const result = extractDeliveryInfo(queriedKey); expect(result).toEqual({ - deliveryContext: { - channel: "matrix", - to: "room:!MixedCase:Example.Org", - accountId: "matrix-account", - }, + deliveryContext: createMixedCaseMatrixDelivery(), threadId: undefined, }); }); diff --git a/src/config/sessions/store.pruning.test.ts b/src/config/sessions/store.pruning.test.ts index fcc8f87b771d..9a71c3e646f9 100644 --- a/src/config/sessions/store.pruning.test.ts +++ b/src/config/sessions/store.pruning.test.ts @@ -368,61 +368,47 @@ describe("applyFileBackedSessionStoreMaintenance", () => { } }); - it("preserves every active admission instead of only the writer session", async () => { + it.each([ + { + name: "preserves every active admission instead of only the writer session", + storeName: "active-admissions", + preserved: [ + ["agent:main:cron:job:run:active", "active-session"], + ["writer", "writer-session"], + ], + identities: ["agent:main:cron:job:run:active", "active-session"], + activeSessionKey: "writer", + }, + { + name: "preserves every store alias backed by an active session id", + storeName: "active-aliases", + preserved: [ + ["agent:main:cron:job:run:active", "active-alias-session"], + ["agent:main:cron:job:run:active:thread:reply", "active-alias-session"], + ], + identities: ["active-alias-session"], + activeSessionKey: undefined, + }, + { + name: "preserves a raw legacy store key matched by a canonical admission identity", + storeName: "active-legacy-key", + preserved: [["Agent:Main:Subagent:CHILD", "active-legacy-session"]], + identities: ["agent:main:subagent:child"], + activeSessionKey: undefined, + }, + ] as const)("$name", async ({ storeName, preserved, identities, activeSessionKey }) => { const now = Date.now(); - const storePath = "/tmp/openclaw-sessions/active-admissions.json"; - const activeKey = "agent:main:cron:job:run:active"; + const storePath = `/tmp/openclaw-sessions/${storeName}.json`; const store = makeStore([ - [activeKey, { sessionId: "active-session", updatedAt: now - 3 }], - ["removable", { sessionId: "removable-session", updatedAt: now - 2 }], - ["writer", { sessionId: "writer-session", updatedAt: now - 1 }], - ]); - const admission = await beginSessionWorkAdmission({ - scope: storePath, - identities: [activeKey, "active-session"], - assertAllowed: () => {}, - }); - - try { - await applyFileBackedSessionStoreMaintenance({ - storePath, - store, - activeSessionKey: "writer", - maintenanceConfig: { - mode: "enforce", - pruneAfterMs: 30 * DAY_MS, - maxEntries: 1, - modelRunPruneAfterMs: DAY_MS, - resetArchiveRetentionMs: null, - maxDiskBytes: null, - highWaterBytes: null, - }, - log: { warn: () => {}, info: () => {} }, - artifacts: createMaintenanceArtifacts(), - }); - - expect(store).toHaveProperty(activeKey); - expect(store).toHaveProperty("writer"); - expect(store.removable).toBeUndefined(); - } finally { - admission.release(); - } - }); - - it("preserves every store alias backed by an active session id", async () => { - const now = Date.now(); - const storePath = "/tmp/openclaw-sessions/active-aliases.json"; - const activeSessionId = "active-alias-session"; - const firstAlias = "agent:main:cron:job:run:active"; - const secondAlias = "agent:main:cron:job:run:active:thread:reply"; - const store = makeStore([ - [firstAlias, { sessionId: activeSessionId, updatedAt: now - 3 }], - [secondAlias, { sessionId: activeSessionId, updatedAt: now - 2 }], + ...preserved.map(([key, sessionId], index): [string, SessionEntry] => [ + key, + { sessionId, updatedAt: now - preserved.length - 1 + index }, + ]), ["removable", { sessionId: "removable-session", updatedAt: now - 1 }], ]); const admission = await beginSessionWorkAdmission({ scope: storePath, - identities: [activeSessionId], + identities: [...identities], assertAllowed: () => {}, }); @@ -430,6 +416,7 @@ describe("applyFileBackedSessionStoreMaintenance", () => { await applyFileBackedSessionStoreMaintenance({ storePath, store, + activeSessionKey, maintenanceConfig: { mode: "enforce", pruneAfterMs: 30 * DAY_MS, @@ -442,48 +429,9 @@ describe("applyFileBackedSessionStoreMaintenance", () => { log: { warn: () => {}, info: () => {} }, artifacts: createMaintenanceArtifacts(), }); - - expect(store).toHaveProperty(firstAlias); - expect(store).toHaveProperty(secondAlias); - expect(store.removable).toBeUndefined(); - } finally { - admission.release(); - } - }); - - it("preserves a raw legacy store key matched by a canonical admission identity", async () => { - const now = Date.now(); - const storePath = "/tmp/openclaw-sessions/active-legacy-key.json"; - const rawActiveKey = "Agent:Main:Subagent:CHILD"; - const canonicalActiveKey = "agent:main:subagent:child"; - const store = makeStore([ - [rawActiveKey, { sessionId: "active-legacy-session", updatedAt: now - 2 }], - ["removable", { sessionId: "removable-session", updatedAt: now - 1 }], - ]); - const admission = await beginSessionWorkAdmission({ - scope: storePath, - identities: [canonicalActiveKey], - assertAllowed: () => {}, - }); - - try { - await applyFileBackedSessionStoreMaintenance({ - storePath, - store, - maintenanceConfig: { - mode: "enforce", - pruneAfterMs: 30 * DAY_MS, - maxEntries: 1, - modelRunPruneAfterMs: DAY_MS, - resetArchiveRetentionMs: null, - maxDiskBytes: null, - highWaterBytes: null, - }, - log: { warn: () => {}, info: () => {} }, - artifacts: createMaintenanceArtifacts(), - }); - - expect(store).toHaveProperty(rawActiveKey); + for (const [key] of preserved) { + expect(store).toHaveProperty(key); + } expect(store.removable).toBeUndefined(); } finally { admission.release(); diff --git a/src/config/validation.channel-metadata.test.ts b/src/config/validation.channel-metadata.test.ts index 31be95e91841..12e6e163052a 100644 --- a/src/config/validation.channel-metadata.test.ts +++ b/src/config/validation.channel-metadata.test.ts @@ -852,16 +852,27 @@ describe("validateConfigObjectRawWithPlugins channel metadata", () => { } }); - it("names the external plugin owner for unsupported channel properties", () => { - mockLoadPluginManifestRegistry.mockReturnValue(createExternalFeishuSchemaRegistry()); - + it.each([ + { + name: "names the external plugin owner for unsupported channel properties", + createRegistry: createExternalFeishuSchemaRegistry, + rejectedOwner: undefined, + }, + { + name: "keeps unsupported property diagnostics assigned to the schema owner", + createRegistry: createExternalFeishuSchemaWithCloserMetadataRegistry, + rejectedOwner: "workspace-channel-labels", + }, + { + name: "keeps schema ownership coupled when closer root metadata preserves a schema", + createRegistry: createExternalFeishuSchemaWithRootOnlyShadowRegistry, + rejectedOwner: "other-global-feishu", + }, + ] as const)("$name", ({ createRegistry, rejectedOwner }) => { + mockLoadPluginManifestRegistry.mockReturnValue(createRegistry()); const result = validateConfigObjectRawWithPlugins({ channels: { - feishu: { - appId: "app-id", - appSecret: "secret", - unsupportedField: true, - }, + feishu: { appId: "app-id", appSecret: "secret", unsupportedField: true }, }, }); @@ -874,66 +885,11 @@ describe("validateConfigObjectRawWithPlugins channel metadata", () => { 'invalid config for plugin openclaw-lark: must not have additional properties: "unsupportedField"', }), ); - } - }); - - it("keeps unsupported property diagnostics assigned to the schema owner", () => { - mockLoadPluginManifestRegistry.mockReturnValue( - createExternalFeishuSchemaWithCloserMetadataRegistry(), - ); - - const result = validateConfigObjectRawWithPlugins({ - channels: { - feishu: { - appId: "app-id", - appSecret: "secret", - unsupportedField: true, - }, - }, - }); - - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.issues).toContainEqual( - expect.objectContaining({ - path: "channels.feishu", - message: - 'invalid config for plugin openclaw-lark: must not have additional properties: "unsupportedField"', - }), - ); - expect(result.issues.map((issue) => issue.message)).not.toContain( - 'invalid config for plugin workspace-channel-labels: must not have additional properties: "unsupportedField"', - ); - } - }); - - it("keeps schema ownership coupled when closer root metadata preserves a schema", () => { - mockLoadPluginManifestRegistry.mockReturnValue( - createExternalFeishuSchemaWithRootOnlyShadowRegistry(), - ); - - const result = validateConfigObjectRawWithPlugins({ - channels: { - feishu: { - appId: "app-id", - appSecret: "secret", - unsupportedField: true, - }, - }, - }); - - expect(result.ok).toBe(false); - if (!result.ok) { - expect(result.issues).toContainEqual( - expect.objectContaining({ - path: "channels.feishu", - message: - 'invalid config for plugin openclaw-lark: must not have additional properties: "unsupportedField"', - }), - ); - expect(result.issues.map((issue) => issue.message)).not.toContain( - 'invalid config for plugin other-global-feishu: must not have additional properties: "unsupportedField"', - ); + if (rejectedOwner) { + expect(result.issues.map((issue) => issue.message)).not.toContain( + `invalid config for plugin ${rejectedOwner}: must not have additional properties: "unsupportedField"`, + ); + } } });