mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
refactor(plugins): remove orphan test API barrels (#121761)
* test(plugins): remove orphan test API barrels * refactor(plugins): remove orphan test-only exports * refactor(imessage): remove orphan test plugin * test(plugins): remove stale package guard timeout
This commit is contained in:
committed by
GitHub
parent
59492c51a5
commit
fd1b965f2b
@@ -1,7 +0,0 @@
|
||||
/**
|
||||
* Test API barrel for Anthropic plugin internals. Tests import this path to
|
||||
* avoid reaching into unrelated runtime modules.
|
||||
*/
|
||||
export { buildAnthropicCliBackend } from "./cli-backend.js";
|
||||
export { normalizeClaudeBackendConfig } from "./cli-shared.js";
|
||||
export { anthropicMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
@@ -1,3 +0,0 @@
|
||||
// Deepgram API module exposes the plugin public contract.
|
||||
export { deepgramMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
export { buildDeepgramRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js";
|
||||
@@ -1,10 +1,7 @@
|
||||
// Elevenlabs tests cover media understanding provider plugin behavior.
|
||||
import { mockPinnedHostnameResolution } from "openclaw/plugin-sdk/test-env";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
elevenLabsMediaUnderstandingProvider,
|
||||
transcribeElevenLabsAudio,
|
||||
} from "./media-understanding-provider.js";
|
||||
import { elevenLabsMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
|
||||
function requireFirstFetchCall(fetchMock: ReturnType<typeof vi.fn>): [string, RequestInit] {
|
||||
const [call] = fetchMock.mock.calls;
|
||||
@@ -38,7 +35,7 @@ describe("elevenLabsMediaUnderstandingProvider", () => {
|
||||
.fn<typeof fetch>()
|
||||
.mockResolvedValue(new Response(JSON.stringify({ text: "hello" })));
|
||||
|
||||
const result = await transcribeElevenLabsAudio({
|
||||
const result = await elevenLabsMediaUnderstandingProvider.transcribeAudio!({
|
||||
buffer: Buffer.from("audio"),
|
||||
fileName: "voice.mp3",
|
||||
mime: "audio/mpeg",
|
||||
@@ -66,7 +63,7 @@ describe("elevenLabsMediaUnderstandingProvider", () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response("{ nope"));
|
||||
|
||||
await expect(
|
||||
transcribeElevenLabsAudio({
|
||||
elevenLabsMediaUnderstandingProvider.transcribeAudio!({
|
||||
buffer: Buffer.from("audio"),
|
||||
fileName: "voice.mp3",
|
||||
mime: "audio/mpeg",
|
||||
@@ -82,7 +79,7 @@ describe("elevenLabsMediaUnderstandingProvider", () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValue(new Response(JSON.stringify([])));
|
||||
|
||||
await expect(
|
||||
transcribeElevenLabsAudio({
|
||||
elevenLabsMediaUnderstandingProvider.transcribeAudio!({
|
||||
buffer: Buffer.from("audio"),
|
||||
fileName: "voice.mp3",
|
||||
mime: "audio/mpeg",
|
||||
|
||||
@@ -16,7 +16,7 @@ import { DEFAULT_ELEVENLABS_BASE_URL, normalizeElevenLabsBaseUrl } from "./share
|
||||
|
||||
const DEFAULT_ELEVENLABS_STT_MODEL = "scribe_v2";
|
||||
|
||||
export async function transcribeElevenLabsAudio(
|
||||
async function transcribeElevenLabsAudio(
|
||||
req: AudioTranscriptionRequest,
|
||||
): Promise<AudioTranscriptionResult> {
|
||||
const fetchFn = req.fetchFn ?? fetch;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Elevenlabs API module exposes the plugin public contract.
|
||||
export {
|
||||
elevenLabsMediaUnderstandingProvider,
|
||||
transcribeElevenLabsAudio,
|
||||
} from "./media-understanding-provider.js";
|
||||
export { buildElevenLabsRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js";
|
||||
export { buildElevenLabsSpeechProvider } from "./speech-provider.js";
|
||||
@@ -1,4 +0,0 @@
|
||||
// Fal API module exposes the plugin public contract.
|
||||
export { buildFalImageGenerationProvider } from "./image-generation-provider.js";
|
||||
export { buildFalMusicGenerationProvider } from "./music-generation-provider.js";
|
||||
export { buildFalVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
@@ -1,3 +0,0 @@
|
||||
// Googlechat API module exposes the plugin public contract.
|
||||
export { googlechatPlugin } from "./src/channel.js";
|
||||
export { setGoogleChatRuntime } from "./src/runtime.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
// Groq API module exposes the plugin public contract.
|
||||
export { groqMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
@@ -1,173 +0,0 @@
|
||||
// Imessage plugin module implements imessage plugin behavior.
|
||||
import type {
|
||||
ChannelMessageActionAdapter,
|
||||
ChannelMessageActionName,
|
||||
ChannelOutboundAdapter,
|
||||
} from "openclaw/plugin-sdk/channel-contract";
|
||||
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/channel-outbound";
|
||||
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
||||
import { collectStatusIssuesFromLastError } from "openclaw/plugin-sdk/status-helpers";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
|
||||
function normalizeIMessageTestHandle(raw: string): string {
|
||||
let trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
while (trimmed) {
|
||||
const lowered = normalizeLowercaseStringOrEmpty(trimmed);
|
||||
if (lowered.startsWith("imessage:")) {
|
||||
trimmed = trimmed.slice("imessage:".length).trim();
|
||||
continue;
|
||||
}
|
||||
if (lowered.startsWith("sms:")) {
|
||||
trimmed = trimmed.slice("sms:".length).trim();
|
||||
continue;
|
||||
}
|
||||
if (lowered.startsWith("auto:")) {
|
||||
trimmed = trimmed.slice("auto:".length).trim();
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (/^(chat_id:|chat_guid:|chat_identifier:)/i.test(trimmed)) {
|
||||
return trimmed.replace(/^(chat_id:|chat_guid:|chat_identifier:)/i, (match) =>
|
||||
normalizeLowercaseStringOrEmpty(match),
|
||||
);
|
||||
}
|
||||
if (trimmed.includes("@")) {
|
||||
return normalizeLowercaseStringOrEmpty(trimmed);
|
||||
}
|
||||
const digits = trimmed.replace(/[^\d+]/g, "");
|
||||
if (digits) {
|
||||
return digits.startsWith("+") ? `+${digits.slice(1)}` : `+${digits}`;
|
||||
}
|
||||
return trimmed.replace(/\s+/g, "");
|
||||
}
|
||||
|
||||
const defaultIMessageOutbound: ChannelOutboundAdapter = {
|
||||
deliveryMode: "direct",
|
||||
deliveryCapabilities: {
|
||||
durableFinal: {
|
||||
text: true,
|
||||
media: true,
|
||||
replyTo: true,
|
||||
messageSendingHooks: true,
|
||||
},
|
||||
},
|
||||
sendText: async ({ to, text, accountId, replyToId, deps, cfg }) => {
|
||||
const sendIMessage = resolveOutboundSendDep<
|
||||
(
|
||||
target: string,
|
||||
content: string,
|
||||
opts?: Record<string, unknown>,
|
||||
) => Promise<{ messageId: string }>
|
||||
>(deps, "imessage");
|
||||
const result = await sendIMessage?.(to, text, {
|
||||
config: cfg,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToId: replyToId ?? undefined,
|
||||
});
|
||||
return { channel: "imessage", messageId: result?.messageId ?? "imessage-test-stub" };
|
||||
},
|
||||
sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, deps, cfg, mediaLocalRoots }) => {
|
||||
const sendIMessage = resolveOutboundSendDep<
|
||||
(
|
||||
target: string,
|
||||
content: string,
|
||||
opts?: Record<string, unknown>,
|
||||
) => Promise<{ messageId: string }>
|
||||
>(deps, "imessage");
|
||||
const result = await sendIMessage?.(to, text, {
|
||||
config: cfg,
|
||||
mediaUrl,
|
||||
accountId: accountId ?? undefined,
|
||||
replyToId: replyToId ?? undefined,
|
||||
mediaLocalRoots,
|
||||
});
|
||||
return { channel: "imessage", messageId: result?.messageId ?? "imessage-test-stub" };
|
||||
},
|
||||
};
|
||||
|
||||
const defaultIMessageActions: ChannelMessageActionAdapter = {
|
||||
describeMessageTool: () => ({
|
||||
actions: [
|
||||
"react",
|
||||
"edit",
|
||||
"unsend",
|
||||
"reply",
|
||||
"sendWithEffect",
|
||||
"upload-file",
|
||||
"renameGroup",
|
||||
"setGroupIcon",
|
||||
"addParticipant",
|
||||
"removeParticipant",
|
||||
"leaveGroup",
|
||||
],
|
||||
}),
|
||||
supportsAction: ({ action }) =>
|
||||
new Set<ChannelMessageActionName>([
|
||||
"react",
|
||||
"edit",
|
||||
"unsend",
|
||||
"reply",
|
||||
"sendWithEffect",
|
||||
"upload-file",
|
||||
"sendAttachment",
|
||||
"renameGroup",
|
||||
"setGroupIcon",
|
||||
"addParticipant",
|
||||
"removeParticipant",
|
||||
"leaveGroup",
|
||||
]).has(action),
|
||||
};
|
||||
|
||||
export const createIMessageTestPlugin = (params?: {
|
||||
outbound?: ChannelOutboundAdapter;
|
||||
actions?: ChannelMessageActionAdapter;
|
||||
}): ChannelPlugin => ({
|
||||
id: "imessage",
|
||||
meta: {
|
||||
id: "imessage",
|
||||
label: "iMessage",
|
||||
selectionLabel: "iMessage (imsg)",
|
||||
docsPath: "/channels/imessage",
|
||||
blurb: "iMessage test stub.",
|
||||
aliases: ["imsg"],
|
||||
},
|
||||
capabilities: { chatTypes: ["direct", "group"], media: true },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({}),
|
||||
},
|
||||
status: {
|
||||
collectStatusIssues: (accounts) => collectStatusIssuesFromLastError("imessage", accounts),
|
||||
},
|
||||
actions: params?.actions ?? defaultIMessageActions,
|
||||
outbound: params?.outbound ?? defaultIMessageOutbound,
|
||||
messaging: {
|
||||
targetResolver: {
|
||||
looksLikeId: (raw) => {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) {
|
||||
return false;
|
||||
}
|
||||
if (/^(imessage:|sms:|auto:|chat_id:|chat_guid:|chat_identifier:)/i.test(trimmed)) {
|
||||
return true;
|
||||
}
|
||||
if (trimmed.includes("@")) {
|
||||
return true;
|
||||
}
|
||||
return /^\+?\d{3,}$/.test(trimmed);
|
||||
},
|
||||
hint: "<handle|chat_id:ID>",
|
||||
},
|
||||
normalizeTarget: (raw) => normalizeIMessageTestHandle(raw),
|
||||
},
|
||||
});
|
||||
@@ -15,32 +15,22 @@ import {
|
||||
} from "openclaw/plugin-sdk/channel-test-helpers";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import { drainPendingDeliveries } from "openclaw/plugin-sdk/delivery-queue-runtime";
|
||||
import {
|
||||
listImportedBundledPluginFacadeIds,
|
||||
resetFacadeRuntimeStateForTest,
|
||||
} from "openclaw/plugin-sdk/plugin-test-runtime";
|
||||
import { withStateDirEnv } from "openclaw/plugin-sdk/test-env";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
clearIMessageApprovalReactionTargetsForTest,
|
||||
resolveIMessageApprovalReactionTargetWithPersistence,
|
||||
} from "./approval-reactions.js";
|
||||
import { imessagePlugin } from "./channel.js";
|
||||
import type { IMessageRpcClient } from "./client.js";
|
||||
import { createIMessageTestPlugin } from "./imessage.test-plugin.js";
|
||||
import { extractMarkdownFormatRuns } from "./markdown-format.js";
|
||||
import { sendMessageIMessage } from "./send.js";
|
||||
|
||||
beforeEach(() => {
|
||||
resetFacadeRuntimeStateForTest();
|
||||
clearIMessageApprovalReactionTargetsForTest();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
resetFacadeRuntimeStateForTest();
|
||||
});
|
||||
|
||||
type IMessageOutbound = NonNullable<ReturnType<typeof createIMessageTestPlugin>["outbound"]>;
|
||||
type IMessageOutbound = NonNullable<typeof imessagePlugin.outbound>;
|
||||
type IMessageMessageAdapter = NonNullable<typeof imessagePlugin.message>;
|
||||
type IMessageMessageSender = NonNullable<IMessageMessageAdapter["send"]>;
|
||||
const IMESSAGE_WORKSPACE_PNG = Buffer.from(
|
||||
@@ -49,7 +39,7 @@ const IMESSAGE_WORKSPACE_PNG = Buffer.from(
|
||||
);
|
||||
|
||||
function requireOutbound(): IMessageOutbound {
|
||||
const outbound = createIMessageTestPlugin().outbound;
|
||||
const outbound = imessagePlugin.outbound;
|
||||
if (!outbound) {
|
||||
throw new Error("Expected iMessage test plugin outbound adapter");
|
||||
}
|
||||
@@ -104,22 +94,7 @@ function requireMessageSendMedia(
|
||||
return media;
|
||||
}
|
||||
|
||||
describe("createIMessageTestPlugin", () => {
|
||||
it("does not load the bundled iMessage facade by default", () => {
|
||||
expect(listImportedBundledPluginFacadeIds()).toStrictEqual([]);
|
||||
|
||||
createIMessageTestPlugin();
|
||||
|
||||
expect(listImportedBundledPluginFacadeIds()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("normalizes repeated transport prefixes without recursive stack growth", () => {
|
||||
const plugin = createIMessageTestPlugin();
|
||||
const prefixedHandle = `${"imessage:".repeat(5000)}+44 20 7946 0958`;
|
||||
|
||||
expect(plugin.messaging?.normalizeTarget?.(prefixedHandle)).toBe("+442079460958");
|
||||
});
|
||||
|
||||
describe("imessagePlugin contracts", () => {
|
||||
it("declares durable final delivery capabilities", () => {
|
||||
expect(imessagePlugin.outbound?.deliveryCapabilities?.durableFinal).toStrictEqual({
|
||||
text: true,
|
||||
@@ -127,12 +102,6 @@ describe("createIMessageTestPlugin", () => {
|
||||
replyTo: true,
|
||||
messageSendingHooks: true,
|
||||
});
|
||||
expect(createIMessageTestPlugin().outbound?.deliveryCapabilities?.durableFinal).toStrictEqual({
|
||||
text: true,
|
||||
media: true,
|
||||
replyTo: true,
|
||||
messageSendingHooks: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves sanitized HTML formatting as native ranges", () => {
|
||||
@@ -705,22 +674,4 @@ describe("createIMessageTestPlugin", () => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
}
|
||||
});
|
||||
|
||||
it("exposes seeded private API actions for binding contract tests", () => {
|
||||
const plugin = createIMessageTestPlugin();
|
||||
|
||||
expect(plugin.actions?.describeMessageTool({} as never)?.actions).toStrictEqual([
|
||||
"react",
|
||||
"edit",
|
||||
"unsend",
|
||||
"reply",
|
||||
"sendWithEffect",
|
||||
"upload-file",
|
||||
"renameGroup",
|
||||
"setGroupIcon",
|
||||
"addParticipant",
|
||||
"removeParticipant",
|
||||
"leaveGroup",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
// Imessage API module exposes the plugin public contract.
|
||||
export { createIMessageTestPlugin } from "./src/imessage.test-plugin.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
// Microsoft API module exposes the plugin public contract.
|
||||
export { buildMicrosoftSpeechProvider } from "./speech-provider.js";
|
||||
@@ -1,3 +0,0 @@
|
||||
// Mistral API module exposes the plugin public contract.
|
||||
export { mistralMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
export { buildMistralRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
// Msteams API module exposes the plugin public contract.
|
||||
export { msteamsPlugin } from "./src/channel.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
// Nostr API module exposes the plugin public contract.
|
||||
export { nostrPlugin } from "./src/channel.js";
|
||||
@@ -1,7 +0,0 @@
|
||||
// Openai API module exposes the plugin public contract.
|
||||
export { buildOpenAIImageGenerationProvider } from "./image-generation-provider.js";
|
||||
export { openaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
export { buildOpenAIRealtimeTranscriptionProvider } from "./realtime-transcription-provider.js";
|
||||
export { buildOpenAIRealtimeVoiceProvider } from "./realtime-voice-provider.js";
|
||||
export { buildOpenAISpeechProvider } from "./speech-provider.js";
|
||||
export { buildOpenAIVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
@@ -1,5 +0,0 @@
|
||||
// Openrouter API module exposes the plugin public contract.
|
||||
export { buildOpenRouterImageGenerationProvider } from "./image-generation-provider.js";
|
||||
export { buildOpenRouterMusicGenerationProvider } from "./music-generation-provider.js";
|
||||
export { openrouterMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
export { buildOpenRouterSpeechProvider } from "./speech-provider.js";
|
||||
@@ -1,3 +0,0 @@
|
||||
// Qwen API module exposes the plugin public contract.
|
||||
export { buildQwenMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
export { qwenVideoGenerationProvider } from "./video-generation-provider.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
// Senseaudio API module exposes the plugin public contract.
|
||||
export { senseaudioMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
@@ -1,3 +0,0 @@
|
||||
// Telegram API module exposes the plugin public contract.
|
||||
export { sendMessageTelegram, sendPollTelegram, type TelegramApiOverride } from "./src/send.js";
|
||||
export { resetTelegramThreadBindingsForTests } from "./src/thread-bindings.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
// Tlon API module exposes the plugin public contract.
|
||||
export { tlonPlugin } from "./src/channel.js";
|
||||
@@ -1,3 +0,0 @@
|
||||
// Whatsapp API module exposes the plugin public contract.
|
||||
export { whatsappOutbound } from "./src/outbound-adapter.js";
|
||||
export { resolveWhatsAppRuntimeGroupPolicy } from "./src/runtime-group-policy.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
// Zai API module exposes the plugin public contract.
|
||||
export { zaiMediaUnderstandingProvider } from "./media-understanding-provider.js";
|
||||
@@ -1,2 +0,0 @@
|
||||
// Zalo API module exposes the plugin public contract.
|
||||
export { resolveZaloRuntimeGroupPolicy } from "./src/group-access.js";
|
||||
@@ -7,7 +7,6 @@ vi.mock("./accounts.js", () => {
|
||||
listZalouserAccountIds: () => ["default"],
|
||||
resolveDefaultZalouserAccountId: () => "default",
|
||||
resolveZalouserAccountSync: () => createDefaultResolvedZalouserAccount(),
|
||||
getZcaUserInfo: async () => null,
|
||||
checkZcaAuthenticated: async () => false,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,20 +1,12 @@
|
||||
// Zalouser tests cover accounts plugin behavior.
|
||||
import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../runtime-api.js";
|
||||
import {
|
||||
getZcaUserInfo,
|
||||
listZalouserAccountIds,
|
||||
resolveDefaultZalouserAccountId,
|
||||
resolveZalouserAccountSync,
|
||||
} from "./accounts.js";
|
||||
import { getZaloUserInfo } from "./zalo-js.js";
|
||||
|
||||
vi.mock("./zalo-js.js", () => ({
|
||||
getZaloUserInfo: vi.fn(),
|
||||
}));
|
||||
|
||||
const mockGetUserInfo = vi.mocked(getZaloUserInfo);
|
||||
const originalZalouserProfile = process.env.ZALOUSER_PROFILE;
|
||||
const originalZcaProfile = process.env.ZCA_PROFILE;
|
||||
|
||||
@@ -24,7 +16,6 @@ function asConfig(value: unknown): OpenClawConfig {
|
||||
|
||||
describe("zalouser account resolution", () => {
|
||||
beforeEach(() => {
|
||||
mockGetUserInfo.mockReset();
|
||||
delete process.env.ZALOUSER_PROFILE;
|
||||
delete process.env.ZCA_PROFILE;
|
||||
});
|
||||
@@ -242,19 +233,4 @@ describe("zalouser account resolution", () => {
|
||||
|
||||
expect(resolveZalouserAccountSync({ cfg, accountId: "work" }).profile).toBe("explicit-profile");
|
||||
});
|
||||
|
||||
it("maps account info helper from zalo-js", async () => {
|
||||
mockGetUserInfo.mockResolvedValueOnce({
|
||||
userId: "123",
|
||||
displayName: "Alice",
|
||||
avatar: "https://example.com/avatar.png",
|
||||
});
|
||||
expect(await getZcaUserInfo("default")).toEqual({
|
||||
userId: "123",
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
mockGetUserInfo.mockResolvedValueOnce(null);
|
||||
expect(await getZcaUserInfo("default")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -77,19 +77,6 @@ export function resolveZalouserAccountSync(params: {
|
||||
};
|
||||
}
|
||||
|
||||
export async function getZcaUserInfo(
|
||||
profile: string,
|
||||
): Promise<{ userId?: string; displayName?: string } | null> {
|
||||
const info = await (await loadZalouserAccountsRuntime()).getZaloUserInfo(profile);
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
userId: info.userId,
|
||||
displayName: info.displayName,
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkZcaAuthenticated(
|
||||
profile: string,
|
||||
options?: { credentialPersistence?: "persist" | "read-only" },
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
// Zalouser API module exposes the plugin public contract.
|
||||
export { sendMessageZalouser } from "./src/send.js";
|
||||
export { parseZalouserOutboundTarget } from "./src/session-route.js";
|
||||
export {
|
||||
checkZcaAuthenticated,
|
||||
getZcaUserInfo,
|
||||
listZalouserAccountIds,
|
||||
resolveDefaultZalouserAccountId,
|
||||
resolveZalouserAccountSync,
|
||||
} from "./src/accounts.js";
|
||||
export {
|
||||
checkZaloAuthenticated,
|
||||
getZaloUserInfo,
|
||||
listZaloFriendsMatching,
|
||||
listZaloGroupMembers,
|
||||
listZaloGroupsMatching,
|
||||
logoutZaloProfile,
|
||||
resolveZaloAllowFromEntries,
|
||||
resolveZaloGroupsByEntries,
|
||||
startZaloQrLogin,
|
||||
waitForZaloQrLogin,
|
||||
} from "./src/zalo-js.js";
|
||||
@@ -0,0 +1,196 @@
|
||||
import fs from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import ts from "typescript";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { listGitTrackedFiles } from "../../test-utils/repo-files.js";
|
||||
|
||||
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
||||
const THIS_TEST_FILE = "src/plugins/contracts/extension-test-api-consumption.test.ts";
|
||||
|
||||
type ExtensionTestApi = {
|
||||
absoluteStem: string;
|
||||
packageName?: string;
|
||||
packageExportsTestApi: boolean;
|
||||
pluginId: string;
|
||||
repoPath: string;
|
||||
};
|
||||
|
||||
function listTrackedFiles(pathspecs: string | readonly string[]): string[] {
|
||||
const files = listGitTrackedFiles({ pathspecs, repoRoot: REPO_ROOT });
|
||||
if (!files) {
|
||||
throw new Error(`failed to list tracked files for ${JSON.stringify(pathspecs)}`);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
function stripModuleExtension(value: string): string {
|
||||
return value.replace(/\.(?:[cm]?[jt]sx?)$/u, "");
|
||||
}
|
||||
|
||||
function listExtensionTestApis(): ExtensionTestApi[] {
|
||||
return listTrackedFiles("extensions/*/test-api.ts").map((repoPath) => {
|
||||
const pluginId = repoPath.split("/")[1];
|
||||
if (!pluginId) {
|
||||
throw new Error(`invalid extension test API path: ${repoPath}`);
|
||||
}
|
||||
const packageJsonPath = resolve(REPO_ROOT, "extensions", pluginId, "package.json");
|
||||
const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")) as {
|
||||
exports?: Record<string, unknown>;
|
||||
name?: unknown;
|
||||
};
|
||||
const packageName = typeof parsed.name === "string" ? parsed.name : undefined;
|
||||
const packageExportsTestApi = Object.keys(parsed.exports ?? {}).some(
|
||||
(key) => stripModuleExtension(key) === "./test-api",
|
||||
);
|
||||
return {
|
||||
absoluteStem: stripModuleExtension(resolve(REPO_ROOT, repoPath)),
|
||||
packageName,
|
||||
packageExportsTestApi,
|
||||
pluginId,
|
||||
repoPath,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function objectStringProperty(node: ts.ObjectLiteralExpression, name: string): string | undefined {
|
||||
for (const property of node.properties) {
|
||||
if (!ts.isPropertyAssignment(property)) {
|
||||
continue;
|
||||
}
|
||||
const propertyName = ts.isIdentifier(property.name)
|
||||
? property.name.text
|
||||
: ts.isStringLiteralLike(property.name)
|
||||
? property.name.text
|
||||
: undefined;
|
||||
if (propertyName === name && ts.isStringLiteralLike(property.initializer)) {
|
||||
return property.initializer.text;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function collectTestApiSourceReferences(source: string, fileName = "source.ts") {
|
||||
const moduleSpecifiers = ts
|
||||
.preProcessFile(source, true, true)
|
||||
.importedFiles.map((entry) => entry.fileName)
|
||||
.toSorted();
|
||||
const pluginIds = new Set<string>();
|
||||
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true);
|
||||
|
||||
function visit(node: ts.Node): void {
|
||||
if (ts.isCallExpression(node)) {
|
||||
const name = ts.isIdentifier(node.expression)
|
||||
? node.expression.text
|
||||
: ts.isPropertyAccessExpression(node.expression)
|
||||
? node.expression.name.text
|
||||
: undefined;
|
||||
const pluginId = node.arguments[0];
|
||||
if (
|
||||
name === "loadQaRunnerBundledPluginTestApi" &&
|
||||
pluginId &&
|
||||
ts.isStringLiteralLike(pluginId)
|
||||
) {
|
||||
pluginIds.add(pluginId.text);
|
||||
}
|
||||
} else if (ts.isObjectLiteralExpression(node)) {
|
||||
const pluginId = objectStringProperty(node, "pluginId");
|
||||
if (pluginId && objectStringProperty(node, "artifactBasename") === "test-api.js") {
|
||||
pluginIds.add(pluginId);
|
||||
}
|
||||
}
|
||||
ts.forEachChild(node, visit);
|
||||
}
|
||||
|
||||
visit(sourceFile);
|
||||
return { moduleSpecifiers, pluginIds: [...pluginIds].toSorted() };
|
||||
}
|
||||
|
||||
function resolveTestApiPluginId(
|
||||
specifier: string,
|
||||
importerFile: string,
|
||||
testApis: readonly ExtensionTestApi[],
|
||||
): string | undefined {
|
||||
if (specifier.startsWith(".") || specifier.startsWith("/")) {
|
||||
const resolvedStem = stripModuleExtension(resolve(dirname(importerFile), specifier));
|
||||
return testApis.find((testApi) => testApi.absoluteStem === resolvedStem)?.pluginId;
|
||||
}
|
||||
const bareSpecifier = stripModuleExtension(specifier);
|
||||
return testApis.find(
|
||||
(testApi) => testApi.packageName && `${testApi.packageName}/test-api` === bareSpecifier,
|
||||
)?.pluginId;
|
||||
}
|
||||
|
||||
function collectOrphanExtensionTestApiFiles(): string[] {
|
||||
const testApis = listExtensionTestApis();
|
||||
const consumed = new Set(
|
||||
testApis.filter((testApi) => testApi.packageExportsTestApi).map((testApi) => testApi.pluginId),
|
||||
);
|
||||
const testApiFiles = new Set(testApis.map((testApi) => testApi.repoPath));
|
||||
|
||||
for (const repoPath of listTrackedFiles(["src", "test", "extensions", "packages", "scripts"])) {
|
||||
if (
|
||||
repoPath === THIS_TEST_FILE ||
|
||||
testApiFiles.has(repoPath) ||
|
||||
!/\.(?:[cm]?[jt]sx?)$/u.test(repoPath)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const absolutePath = resolve(REPO_ROOT, repoPath);
|
||||
const source = fs.readFileSync(absolutePath, "utf8");
|
||||
if (!source.includes("test-api") && !source.includes("loadQaRunnerBundledPluginTestApi")) {
|
||||
continue;
|
||||
}
|
||||
const references = collectTestApiSourceReferences(source, absolutePath);
|
||||
for (const pluginId of references.pluginIds) {
|
||||
if (testApis.some((testApi) => testApi.pluginId === pluginId)) {
|
||||
consumed.add(pluginId);
|
||||
}
|
||||
}
|
||||
for (const specifier of references.moduleSpecifiers) {
|
||||
const pluginId = resolveTestApiPluginId(specifier, absolutePath, testApis);
|
||||
if (pluginId) {
|
||||
consumed.add(pluginId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return testApis
|
||||
.filter((testApi) => !consumed.has(testApi.pluginId))
|
||||
.map((testApi) => testApi.repoPath)
|
||||
.toSorted();
|
||||
}
|
||||
|
||||
describe("extension test API consumption", () => {
|
||||
it("ignores identifier text and generic loader implementations", () => {
|
||||
expect(
|
||||
collectTestApiSourceReferences(`
|
||||
const testing = {};
|
||||
function loadQaRunnerBundledPluginTestApi(pluginId: string) {
|
||||
return load({ pluginId, artifactBasename: "test-api.js" });
|
||||
}
|
||||
`),
|
||||
).toStrictEqual({ moduleSpecifiers: [], pluginIds: [] });
|
||||
});
|
||||
|
||||
it("collects real module edges and literal plugin loaders", () => {
|
||||
expect(
|
||||
collectTestApiSourceReferences(`
|
||||
import { testing } from "@openclaw/example/test-api.js";
|
||||
export { helper } from "../test-api.js";
|
||||
type TestApi = typeof import("./test-api.js");
|
||||
loadQaRunnerBundledPluginTestApi("matrix");
|
||||
loadBundledPluginPublicSurface({ pluginId: "codex", artifactBasename: "test-api.js" });
|
||||
`),
|
||||
).toStrictEqual({
|
||||
moduleSpecifiers: ["../test-api.js", "./test-api.js", "@openclaw/example/test-api.js"],
|
||||
pluginIds: ["codex", "matrix"],
|
||||
});
|
||||
});
|
||||
|
||||
it(
|
||||
"keeps only consumed extension test APIs",
|
||||
() => expect(collectOrphanExtensionTestApiFiles()).toStrictEqual([]),
|
||||
240_000,
|
||||
);
|
||||
});
|
||||
@@ -36,7 +36,6 @@ const PRIVATE_BUNDLED_SDK_SURFACE_PATTERN =
|
||||
const GENERIC_CORE_HELPER_FILES = ["src/polls.ts", "src/poll-params.ts"] as const;
|
||||
const GENERIC_CORE_PLUGIN_OWNER_NAME_PATTERN =
|
||||
/\b(?:imessage|discord|feishu|googlechat|matrix|mattermost|msteams|slack|telegram|whatsapp|zalo|zalouser)\b/gi;
|
||||
const PACKAGE_CONTRACT_SCAN_TIMEOUT_MS = 240_000;
|
||||
const DEPRECATED_EXTENSION_SDK_SPECIFIERS = new Set([
|
||||
"openclaw/plugin-sdk",
|
||||
// Bundled code uses the canonical channel-config-schema subpath; the
|
||||
@@ -539,38 +538,6 @@ function collectDeprecatedTestAliasImports(): string[] {
|
||||
return leaks.map((entry) => `${entry.file}: ${entry.specifier}`).toSorted();
|
||||
}
|
||||
|
||||
function parseTestApiNamedExports(source: string): string[] {
|
||||
const exports = new Set<string>();
|
||||
const declarationPattern =
|
||||
/\bexport\s+(?:const|function|class|async\s+function|type|interface)\s+([A-Za-z_$][\w$]*)/g;
|
||||
const exportListPattern = /\bexport\s*\{([^}]+)\}/g;
|
||||
|
||||
for (const match of source.matchAll(declarationPattern)) {
|
||||
const exportName = match[1];
|
||||
if (exportName) {
|
||||
exports.add(exportName);
|
||||
}
|
||||
}
|
||||
|
||||
for (const match of source.matchAll(exportListPattern)) {
|
||||
const exportList = match[1];
|
||||
if (!exportList) {
|
||||
continue;
|
||||
}
|
||||
for (const part of exportList.split(",")) {
|
||||
const item = part.trim().replace(/^type\s+/, "");
|
||||
const aliasMatch = /\bas\s+([A-Za-z_$][\w$]*)$/u.exec(item);
|
||||
const nameMatch = /^([A-Za-z_$][\w$]*)/u.exec(item);
|
||||
const exportName = aliasMatch?.[1] ?? nameMatch?.[1];
|
||||
if (exportName && exportName !== "default") {
|
||||
exports.add(exportName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [...exports].toSorted();
|
||||
}
|
||||
|
||||
function collectWorkspaceCodeFiles(): string[] {
|
||||
const files: string[] = [];
|
||||
for (const root of ["src", "test", "extensions", "packages", "scripts"]) {
|
||||
@@ -582,74 +549,6 @@ function collectWorkspaceCodeFiles(): string[] {
|
||||
return files;
|
||||
}
|
||||
|
||||
function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function collectUnusedExtensionTestApiExports(): Array<{ file: string; exportName: string }> {
|
||||
const leaks: Array<{ file: string; exportName: string }> = [];
|
||||
const workspaceCodeFiles = collectWorkspaceCodeFiles();
|
||||
const testApiFiles = collectCodeFiles(resolve(REPO_ROOT, "extensions")).filter((file) =>
|
||||
file.endsWith("/test-api.ts"),
|
||||
);
|
||||
const testApiExports = new Map<string, string[]>();
|
||||
const exportNames = new Set<string>();
|
||||
|
||||
for (const file of testApiFiles) {
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
const namedExports = parseTestApiNamedExports(source);
|
||||
testApiExports.set(file, namedExports);
|
||||
for (const exportName of namedExports) {
|
||||
exportNames.add(exportName);
|
||||
}
|
||||
}
|
||||
|
||||
if (exportNames.size === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const identifierPattern = new RegExp(
|
||||
`\\b(${[...exportNames].map(escapeRegExp).join("|")})\\b`,
|
||||
"g",
|
||||
);
|
||||
const referenceCounts = new Map<string, number>();
|
||||
const selfReferenceCounts = new Map<string, Map<string, number>>();
|
||||
|
||||
for (const file of workspaceCodeFiles) {
|
||||
const source = fs.readFileSync(file, "utf8");
|
||||
const selfCounts = testApiExports.has(file) ? new Map<string, number>() : undefined;
|
||||
for (const match of source.matchAll(identifierPattern)) {
|
||||
const exportName = match[1];
|
||||
if (!exportName) {
|
||||
continue;
|
||||
}
|
||||
referenceCounts.set(exportName, (referenceCounts.get(exportName) ?? 0) + 1);
|
||||
if (selfCounts) {
|
||||
selfCounts.set(exportName, (selfCounts.get(exportName) ?? 0) + 1);
|
||||
}
|
||||
}
|
||||
if (selfCounts) {
|
||||
selfReferenceCounts.set(file, selfCounts);
|
||||
}
|
||||
}
|
||||
|
||||
for (const [file, namedExports] of testApiExports) {
|
||||
const repoRelativePath = toRepoRelativePath(file);
|
||||
for (const exportName of namedExports) {
|
||||
const referenceCount =
|
||||
(referenceCounts.get(exportName) ?? 0) -
|
||||
(selfReferenceCounts.get(file)?.get(exportName) ?? 0);
|
||||
if (referenceCount === 0) {
|
||||
leaks.push({ file: repoRelativePath, exportName });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return leaks.toSorted(
|
||||
(a, b) => a.file.localeCompare(b.file) || a.exportName.localeCompare(b.exportName),
|
||||
);
|
||||
}
|
||||
|
||||
function collectCrossOwnerReservedSdkImports(): Array<{
|
||||
file: string;
|
||||
specifier: string;
|
||||
@@ -999,14 +898,6 @@ describe("plugin-sdk package contract guardrails", () => {
|
||||
expect(deprecatedTestAliasImports).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it(
|
||||
"keeps extension test-api exports consumed",
|
||||
() => {
|
||||
expect(collectUnusedExtensionTestApiExports()).toStrictEqual([]);
|
||||
},
|
||||
PACKAGE_CONTRACT_SCAN_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
it("keeps reserved SDK compatibility subpaths inside their owning bundled plugins", () => {
|
||||
expect(collectCrossOwnerReservedSdkImports()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user