test(contracts): route bundled contract tests through sdk facades

This commit is contained in:
Vincent Koc
2026-04-06 11:35:40 +01:00
parent 47f0dc3adb
commit 4154bd707a
16 changed files with 142 additions and 27 deletions

View File

@@ -3,7 +3,10 @@ import type { OpenClawConfig } from "../../../config/config.js";
import { listBundledChannelPlugins, setBundledChannelRuntime } from "../bundled.js";
import type { ChannelPlugin } from "../types.js";
import { channelPluginSurfaceKeys, type ChannelPluginSurface } from "./manifest.js";
import { importBundledChannelContractArtifact } from "./runtime-artifacts.js";
import {
importBundledChannelContractArtifact,
resolveBundledChannelContractArtifactUrl,
} from "./runtime-artifacts.js";
type SurfaceContractEntry = {
id: string;
@@ -41,8 +44,8 @@ const sendMessageMatrixMock = vi.hoisted(() =>
roomId: to.replace(/^room:/, ""),
})),
);
const matrixRuntimeApiModuleId = vi.hoisted(
() => new URL("../../../../extensions/matrix/runtime-api.js", import.meta.url).href,
const matrixRuntimeApiModuleId = vi.hoisted(() =>
resolveBundledChannelContractArtifactUrl("matrix", "runtime-api.js"),
);
const lineContractApi = await importBundledChannelContractArtifact<{

View File

@@ -1,11 +1,11 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../../config/config.js";
import {
namedAccountPromotionKeys as matrixNamedAccountPromotionKeys,
resolveSingleAccountPromotionTarget as resolveMatrixSingleAccountPromotionTarget,
singleAccountKeysToMove as matrixSingleAccountKeysToMove,
} from "../../../extensions/matrix/contract-api.js";
import { singleAccountKeysToMove as telegramSingleAccountKeysToMove } from "../../../extensions/telegram/contract-api.js";
import type { OpenClawConfig } from "../../config/config.js";
} from "../../plugin-sdk/matrix.js";
import { singleAccountKeysToMove as telegramSingleAccountKeysToMove } from "../../plugin-sdk/telegram.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import {

View File

@@ -1,15 +1,15 @@
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
namedAccountPromotionKeys as matrixNamedAccountPromotionKeys,
resolveSingleAccountPromotionTarget as resolveMatrixSingleAccountPromotionTarget,
singleAccountKeysToMove as matrixSingleAccountKeysToMove,
} from "../../../extensions/matrix/contract-api.js";
import { singleAccountKeysToMove as telegramSingleAccountKeysToMove } from "../../../extensions/telegram/contract-api.js";
import {
resolveSetupWizardAllowFromEntries,
resolveSetupWizardGroupAllowlist,
} from "../../../test/helpers/plugins/setup-wizard.js";
import type { OpenClawConfig } from "../../config/config.js";
import {
namedAccountPromotionKeys as matrixNamedAccountPromotionKeys,
resolveSingleAccountPromotionTarget as resolveMatrixSingleAccountPromotionTarget,
singleAccountKeysToMove as matrixSingleAccountKeysToMove,
} from "../../plugin-sdk/matrix.js";
import { singleAccountKeysToMove as telegramSingleAccountKeysToMove } from "../../plugin-sdk/telegram.js";
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../../plugins/runtime.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import {

16
src/plugin-sdk/discord.ts Normal file
View File

@@ -0,0 +1,16 @@
// Manual facade. Keep loader boundary explicit.
type FacadeModule = typeof import("@openclaw/discord/contract-api.js");
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
function loadFacadeModule(): FacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
dirName: "discord",
artifactBasename: "contract-api.js",
});
}
export const collectDiscordSecurityAuditFindings: FacadeModule["collectDiscordSecurityAuditFindings"] =
((...args) =>
loadFacadeModule().collectDiscordSecurityAuditFindings(
...args,
)) as FacadeModule["collectDiscordSecurityAuditFindings"];

View File

@@ -2,6 +2,16 @@
// Keep this list additive and scoped to the bundled Matrix surface.
import { createOptionalChannelSetupSurface } from "./channel-setup.js";
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
type MatrixFacadeModule = typeof import("@openclaw/matrix/contract-api.js");
function loadMatrixFacadeModule(): MatrixFacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<MatrixFacadeModule>({
dirName: "matrix",
artifactBasename: "contract-api.js",
});
}
export {
createActionGate,
@@ -179,6 +189,18 @@ export {
} from "./matrix-surface.js";
export { setMatrixRuntime } from "./matrix-runtime-surface.js";
export const singleAccountKeysToMove: MatrixFacadeModule["singleAccountKeysToMove"] =
loadMatrixFacadeModule().singleAccountKeysToMove;
export const namedAccountPromotionKeys: MatrixFacadeModule["namedAccountPromotionKeys"] =
loadMatrixFacadeModule().namedAccountPromotionKeys;
export const resolveSingleAccountPromotionTarget: MatrixFacadeModule["resolveSingleAccountPromotionTarget"] =
((...args) =>
loadMatrixFacadeModule().resolveSingleAccountPromotionTarget(
...args,
)) as MatrixFacadeModule["resolveSingleAccountPromotionTarget"];
const matrixSetup = createOptionalChannelSetupSurface({
channel: "matrix",
label: "Matrix",

View File

@@ -0,0 +1,16 @@
// Manual facade. Keep loader boundary explicit.
type FacadeModule = typeof import("@openclaw/synology-chat/contract-api.js");
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
function loadFacadeModule(): FacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
dirName: "synology-chat",
artifactBasename: "contract-api.js",
});
}
export const collectSynologyChatSecurityAuditFindings: FacadeModule["collectSynologyChatSecurityAuditFindings"] =
((...args) =>
loadFacadeModule().collectSynologyChatSecurityAuditFindings(
...args,
)) as FacadeModule["collectSynologyChatSecurityAuditFindings"];

View File

@@ -15,3 +15,12 @@ export const parseTelegramTopicConversation: FacadeModule["parseTelegramTopicCon
loadFacadeModule().parseTelegramTopicConversation(
...args,
)) as FacadeModule["parseTelegramTopicConversation"];
export const singleAccountKeysToMove: FacadeModule["singleAccountKeysToMove"] =
loadFacadeModule().singleAccountKeysToMove;
export const collectTelegramSecurityAuditFindings: FacadeModule["collectTelegramSecurityAuditFindings"] =
((...args) =>
loadFacadeModule().collectTelegramSecurityAuditFindings(
...args,
)) as FacadeModule["collectTelegramSecurityAuditFindings"];

View File

@@ -78,6 +78,22 @@ export { formatResolvedUnresolvedNote } from "./resolution-notes.js";
export { buildBaseAccountStatusSnapshot } from "./status-helpers.js";
export { chunkTextForOutbound } from "./text-chunking.js";
type FacadeModule = typeof import("@openclaw/zalouser/contract-api.js");
import { loadBundledPluginPublicSurfaceModuleSync } from "./facade-runtime.js";
function loadFacadeModule(): FacadeModule {
return loadBundledPluginPublicSurfaceModuleSync<FacadeModule>({
dirName: "zalouser",
artifactBasename: "contract-api.js",
});
}
export const collectZalouserSecurityAuditFindings: FacadeModule["collectZalouserSecurityAuditFindings"] =
((...args) =>
loadFacadeModule().collectZalouserSecurityAuditFindings(
...args,
)) as FacadeModule["collectZalouserSecurityAuditFindings"];
const zalouserSetup = createOptionalChannelSetupSurface({
channel: "zalouser",
label: "Zalo Personal",

View File

@@ -12,6 +12,7 @@ const ALLOWED_BUNDLED_CAPABILITY_METADATA_CONSUMERS = new Set([
]);
const ALLOWED_EXTENSION_PATH_STRING_TESTS = new Set([
"src/plugin-sdk/browser-maintenance.test.ts",
"src/channels/plugins/bundled.shape-guard.test.ts",
"src/plugins/contracts/bundled-extension-config-api-guardrails.test.ts",
"src/scripts/test-projects.test.ts",

View File

@@ -41,6 +41,7 @@ const EXPECTED_SHARED_FAMILY_CONTRACTS: Record<string, ExpectedSharedFamilyContr
google: {
replayFamilies: ["google-gemini"],
streamFamilies: ["google-thinking"],
toolCompatFamilies: ["gemini"],
},
kilocode: {
replayFamilies: ["passthrough-gemini"],

View File

@@ -1,9 +1,9 @@
import fs from "node:fs/promises";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { collectDiscordSecurityAuditFindings } from "../../extensions/discord/contract-api.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { collectDiscordSecurityAuditFindings } from "../plugin-sdk/discord.js";
import { withChannelSecurityStateDir } from "./audit-channel-security.test-helpers.js";
import { collectChannelSecurityFindings } from "./audit-channel.js";

View File

@@ -1,10 +1,12 @@
import { describe, expect, it, vi } from "vitest";
import { collectDiscordSecurityAuditFindings } from "../../extensions/discord/contract-api.js";
import type { ResolvedDiscordAccount } from "../../extensions/discord/src/accounts.js";
import type { DiscordAccountConfig } from "../../extensions/discord/src/runtime-api.js";
import type { OpenClawConfig } from "../config/config.js";
import { collectDiscordSecurityAuditFindings } from "../plugin-sdk/discord.js";
import { withChannelSecurityStateDir } from "./audit-channel-security.test-helpers.js";
type DiscordAuditParams = Parameters<typeof collectDiscordSecurityAuditFindings>[0];
type ResolvedDiscordAccount = DiscordAuditParams["account"];
type DiscordAccountConfig = ResolvedDiscordAccount["config"];
const { readChannelAllowFromStoreMock } = vi.hoisted(() => ({
readChannelAllowFromStoreMock: vi.fn(async () => [] as string[]),
}));

View File

@@ -1,8 +1,10 @@
import { describe, expect, it, vi } from "vitest";
import { collectDiscordSecurityAuditFindings } from "../../extensions/discord/contract-api.js";
import type { ResolvedDiscordAccount } from "../../extensions/discord/src/accounts.js";
import type { DiscordAccountConfig } from "../../extensions/discord/src/runtime-api.js";
import type { OpenClawConfig } from "../config/config.js";
import { collectDiscordSecurityAuditFindings } from "../plugin-sdk/discord.js";
type DiscordAuditParams = Parameters<typeof collectDiscordSecurityAuditFindings>[0];
type ResolvedDiscordAccount = DiscordAuditParams["account"];
type DiscordAccountConfig = ResolvedDiscordAccount["config"];
const { readChannelAllowFromStoreMock } = vi.hoisted(() => ({
readChannelAllowFromStoreMock: vi.fn(async () => [] as string[]),

View File

@@ -1,7 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import { collectDiscordSecurityAuditFindings } from "../../extensions/discord/contract-api.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { collectDiscordSecurityAuditFindings } from "../plugin-sdk/discord.js";
import { collectChannelSecurityFindings } from "./audit-channel.js";
const { readChannelAllowFromStoreMock } = vi.hoisted(() => ({

View File

@@ -1,13 +1,16 @@
import { describe, expect, it } from "vitest";
import { collectSynologyChatSecurityAuditFindings } from "../../extensions/synology-chat/contract-api.js";
import { resolveAccount as resolveSynologyChatAccount } from "../../extensions/synology-chat/src/accounts.js";
import { collectZalouserSecurityAuditFindings } from "../../extensions/zalouser/contract-api.js";
import type { ResolvedZalouserAccount } from "../../extensions/zalouser/src/accounts.js";
import type { ChannelPlugin } from "../channels/plugins/types.js";
import type { OpenClawConfig } from "../config/config.js";
import { collectSynologyChatSecurityAuditFindings } from "../plugin-sdk/synology-chat.js";
import { collectZalouserSecurityAuditFindings } from "../plugin-sdk/zalouser.js";
import { withChannelSecurityStateDir } from "./audit-channel-security.test-helpers.js";
import { collectChannelSecurityFindings } from "./audit-channel.js";
type SynologyAuditParams = Parameters<typeof collectSynologyChatSecurityAuditFindings>[0];
type ResolvedSynologyChatAccount = SynologyAuditParams["account"];
type ZalouserAuditParams = Parameters<typeof collectZalouserSecurityAuditFindings>[0];
type ResolvedZalouserAccount = ZalouserAuditParams["account"];
function stubZalouserPlugin(): ChannelPlugin {
return {
id: "zalouser",
@@ -44,6 +47,28 @@ function stubZalouserPlugin(): ChannelPlugin {
};
}
function createSynologyChatAccount(params: {
cfg: OpenClawConfig;
accountId: string;
}): ResolvedSynologyChatAccount {
const channel = params.cfg.channels?.["synology-chat"] ?? {};
const accountConfig =
params.accountId === "default"
? channel
: ((channel.accounts as Record<string, unknown> | undefined)?.[params.accountId] ?? {});
return {
accountId: params.accountId,
dangerouslyAllowNameMatching:
Boolean(
(accountConfig as { dangerouslyAllowNameMatching?: boolean }).dangerouslyAllowNameMatching,
) ||
Boolean(
params.accountId === "default" &&
(channel as { dangerouslyAllowNameMatching?: boolean }).dangerouslyAllowNameMatching,
),
} as ResolvedSynologyChatAccount;
}
describe("security audit synology and zalo channel routing", () => {
it.each([
{
@@ -100,7 +125,7 @@ describe("security audit synology and zalo channel routing", () => {
? "beta"
: "default";
const findings = collectSynologyChatSecurityAuditFindings({
account: resolveSynologyChatAccount(testCase.cfg, accountId),
account: createSynologyChatAccount({ cfg: testCase.cfg, accountId }),
accountId,
orderedAccountIds: Object.keys(synologyChat.accounts ?? {}),
hasExplicitAccountPath: accountId !== "default",

View File

@@ -1,9 +1,11 @@
import { describe, expect, it, vi } from "vitest";
import { collectTelegramSecurityAuditFindings } from "../../extensions/telegram/contract-api.js";
import type { ResolvedTelegramAccount } from "../../extensions/telegram/src/accounts.js";
import type { OpenClawConfig } from "../config/config.js";
import { collectTelegramSecurityAuditFindings } from "../plugin-sdk/telegram.js";
import { withChannelSecurityStateDir } from "./audit-channel-security.test-helpers.js";
type TelegramAuditParams = Parameters<typeof collectTelegramSecurityAuditFindings>[0];
type ResolvedTelegramAccount = TelegramAuditParams["account"];
const { readChannelAllowFromStoreMock } = vi.hoisted(() => ({
readChannelAllowFromStoreMock: vi.fn(async () => [] as string[]),
}));