mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-20 21:02:10 +08:00
refactor: enforce plugin-owned channel boundaries
This commit is contained in:
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
@@ -1264,6 +1264,7 @@ jobs:
|
||||
run_check "plugin-extension-boundary" pnpm run lint:plugins:no-extension-imports
|
||||
run_check "lint:tmp:no-random-messaging" pnpm run lint:tmp:no-random-messaging
|
||||
run_check "lint:tmp:channel-agnostic-boundaries" pnpm run lint:tmp:channel-agnostic-boundaries
|
||||
run_check "lint:tmp:tsgo-core-boundary" pnpm run lint:tmp:tsgo-core-boundary
|
||||
run_check "lint:tmp:no-raw-channel-fetch" pnpm run lint:tmp:no-raw-channel-fetch
|
||||
run_check "lint:agent:ingress-owner" pnpm run lint:agent:ingress-owner
|
||||
run_check "lint:plugins:no-register-http-handler" pnpm run lint:plugins:no-register-http-handler
|
||||
|
||||
@@ -130,10 +130,11 @@
|
||||
- TypeScript checks are split by architecture boundary:
|
||||
- `pnpm tsgo` / `pnpm tsgo:core`: core production roots (`src/`, `ui/`, `packages/`; no `extensions/` include roots).
|
||||
- `pnpm tsgo:core:test`: core colocated tests.
|
||||
- `pnpm tsgo:core:test:agents` / `pnpm tsgo:core:test:non-agents`: core test slices for isolating the heavy agent graph while debugging type-check performance.
|
||||
- `pnpm tsgo:extensions`: bundled extension production graph.
|
||||
- `pnpm tsgo:extensions:test`: bundled extension colocated tests.
|
||||
- `pnpm tsgo:all`: every TypeScript graph above; this is what `pnpm check` runs.
|
||||
- `pnpm tsgo:profile [core-test|extensions-test|--all]`: profile fresh graph cost into `.artifacts/tsgo-profile/`; if a core graph lists `extensions/<id>/`, treat that as boundary/perf debt from imports (usually plugin-sdk facades or shared helpers pulling extension sources).
|
||||
- `pnpm tsgo:profile [core-test|core-test-agents|core-test-non-agents|extensions-test|--all]`: profile fresh graph cost into `.artifacts/tsgo-profile/`; if a core graph lists `extensions/<id>/`, treat that as boundary/perf debt from imports (usually plugin-sdk facades or shared helpers pulling extension sources).
|
||||
- Narrow aliases remain for local loops: `pnpm tsgo:test:src`, `pnpm tsgo:test:ui`, `pnpm tsgo:test:packages`.
|
||||
- Do not add `tsc --noEmit`, `typecheck`, or `check:types` lanes for repo type checking. Use `tsgo` graphs. `tsc` is allowed only when emitting declaration/package-boundary compatibility artifacts that `tsgo` does not replace.
|
||||
- Boundary rule: core must not know extension implementation details. Extensions hook into core through manifests, registries, capabilities, and public `openclaw/plugin-sdk/*` contracts. If you find core production code naming a specific extension, or a core test that is really testing extension-owned behavior, call it out and prefer moving coverage/logic to the owning extension or a generic contract test.
|
||||
|
||||
@@ -288,6 +288,13 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
|
||||
messaging: {
|
||||
defaultMarkdownTableMode: "off",
|
||||
normalizeTarget: normalizeMattermostMessagingTarget,
|
||||
resolveDeliveryTarget: ({ conversationId, parentConversationId }) => {
|
||||
const parent = parentConversationId?.trim();
|
||||
const child = conversationId.trim();
|
||||
return parent && parent !== child
|
||||
? { to: `channel:${parent}`, threadId: child }
|
||||
: { to: normalizeMattermostMessagingTarget(`channel:${child}`) };
|
||||
},
|
||||
resolveOutboundSessionRoute: (params) => resolveMattermostOutboundSessionRoute(params),
|
||||
targetResolver: {
|
||||
looksLikeId: looksLikeMattermostTargetId,
|
||||
|
||||
@@ -333,6 +333,13 @@ export const slackPlugin: ChannelPlugin<ResolvedSlackAccount, SlackProbe> = crea
|
||||
},
|
||||
messaging: {
|
||||
normalizeTarget: normalizeSlackMessagingTarget,
|
||||
resolveDeliveryTarget: ({ conversationId, parentConversationId }) => {
|
||||
const parent = parentConversationId?.trim();
|
||||
const child = conversationId.trim();
|
||||
return parent && parent !== child
|
||||
? { to: `channel:${parent}`, threadId: child }
|
||||
: { to: normalizeSlackMessagingTarget(`channel:${child}`) };
|
||||
},
|
||||
resolveSessionTarget: ({ id }) => normalizeSlackMessagingTarget(`channel:${id}`),
|
||||
parseExplicitTarget: ({ raw }) => parseSlackExplicitTarget(raw),
|
||||
inferTargetChatType: ({ to }) => parseSlackExplicitTarget(to)?.chatType,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { canonicalizeLegacySessionKey, isLegacyGroupSessionKey } from "./session-contract.js";
|
||||
import {
|
||||
canonicalizeLegacySessionKey,
|
||||
deriveLegacySessionChatType,
|
||||
isLegacyGroupSessionKey,
|
||||
} from "./session-contract.js";
|
||||
|
||||
describe("whatsapp legacy session contract", () => {
|
||||
it("canonicalizes legacy WhatsApp group keys to channel-qualified agent keys", () => {
|
||||
@@ -16,6 +20,12 @@ describe("whatsapp legacy session contract", () => {
|
||||
|
||||
it("does not claim generic non-WhatsApp group keys", () => {
|
||||
expect(isLegacyGroupSessionKey("group:abc")).toBe(false);
|
||||
expect(deriveLegacySessionChatType("group:abc")).toBeUndefined();
|
||||
expect(canonicalizeLegacySessionKey({ key: "group:abc", agentId: "main" })).toBeNull();
|
||||
});
|
||||
|
||||
it("derives chat type for legacy WhatsApp group keys", () => {
|
||||
expect(deriveLegacySessionChatType("123@g.us")).toBe("group");
|
||||
expect(deriveLegacySessionChatType("whatsapp:123@g.us")).toBe("group");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -28,6 +28,10 @@ export function isLegacyGroupSessionKey(key: string): boolean {
|
||||
return extractLegacyWhatsAppGroupId(key) !== null;
|
||||
}
|
||||
|
||||
export function deriveLegacySessionChatType(key: string): "group" | undefined {
|
||||
return isLegacyGroupSessionKey(key) ? "group" : undefined;
|
||||
}
|
||||
|
||||
export function canonicalizeLegacySessionKey(params: {
|
||||
key: string;
|
||||
agentId: string;
|
||||
|
||||
@@ -32,7 +32,11 @@ import {
|
||||
unsupportedSecretRefSurfacePatterns,
|
||||
} from "./security-contract.js";
|
||||
import { applyWhatsAppSecurityConfigFixes } from "./security-fix.js";
|
||||
import { canonicalizeLegacySessionKey, isLegacyGroupSessionKey } from "./session-contract.js";
|
||||
import {
|
||||
canonicalizeLegacySessionKey,
|
||||
deriveLegacySessionChatType,
|
||||
isLegacyGroupSessionKey,
|
||||
} from "./session-contract.js";
|
||||
|
||||
export const WHATSAPP_CHANNEL = "whatsapp" as const;
|
||||
|
||||
@@ -245,6 +249,7 @@ export function createWhatsAppPluginBase(params: {
|
||||
config: base.config!,
|
||||
messaging: {
|
||||
defaultMarkdownTableMode: "bullets",
|
||||
deriveLegacySessionChatType,
|
||||
resolveLegacyGroupSessionKey,
|
||||
isLegacyGroupSessionKey,
|
||||
canonicalizeLegacySessionKey: (params) =>
|
||||
|
||||
@@ -1321,6 +1321,7 @@
|
||||
"lint:tmp:dynamic-import-warts": "node scripts/check-dynamic-import-warts.mjs",
|
||||
"lint:tmp:no-random-messaging": "node scripts/check-no-random-messaging-tmp.mjs",
|
||||
"lint:tmp:no-raw-channel-fetch": "node scripts/check-no-raw-channel-fetch.mjs",
|
||||
"lint:tmp:tsgo-core-boundary": "node scripts/check-tsgo-core-boundary.mjs",
|
||||
"lint:ui:no-raw-window-open": "node scripts/check-no-raw-window-open.mjs",
|
||||
"lint:web-fetch-provider-boundaries": "node scripts/check-web-fetch-provider-boundaries.mjs",
|
||||
"lint:web-search-provider-boundaries": "node scripts/check-web-search-provider-boundaries.mjs",
|
||||
@@ -1473,6 +1474,8 @@
|
||||
"tsgo:all": "pnpm tsgo:core && pnpm tsgo:core:test && pnpm tsgo:extensions && pnpm tsgo:extensions:test",
|
||||
"tsgo:core": "node scripts/run-tsgo.mjs -p tsconfig.core.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core.tsbuildinfo",
|
||||
"tsgo:core:test": "node scripts/run-tsgo.mjs -p tsconfig.core.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test.tsbuildinfo",
|
||||
"tsgo:core:test:agents": "node scripts/run-tsgo.mjs -p tsconfig.core.test.agents.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-agents.tsbuildinfo",
|
||||
"tsgo:core:test:non-agents": "node scripts/run-tsgo.mjs -p tsconfig.core.test.non-agents.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/core-test-non-agents.tsbuildinfo",
|
||||
"tsgo:extensions": "node scripts/run-tsgo.mjs -p tsconfig.extensions.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions.tsbuildinfo",
|
||||
"tsgo:extensions:test": "node scripts/run-tsgo.mjs -p tsconfig.extensions.test.json --incremental --tsBuildInfoFile .artifacts/tsgo-cache/extensions-test.tsbuildinfo",
|
||||
"tsgo:prod": "pnpm tsgo:core && pnpm tsgo:extensions",
|
||||
|
||||
@@ -23,6 +23,9 @@ const acpCoreProtectedSources = [
|
||||
const channelCoreProtectedSources = [
|
||||
path.join(repoRoot, "src", "channels", "thread-bindings-policy.ts"),
|
||||
path.join(repoRoot, "src", "channels", "thread-bindings-messages.ts"),
|
||||
path.join(repoRoot, "src", "sessions", "send-policy.ts"),
|
||||
path.join(repoRoot, "src", "sessions", "session-chat-type-shared.ts"),
|
||||
path.join(repoRoot, "src", "utils", "delivery-context.ts"),
|
||||
];
|
||||
const acpUserFacingTextSources = [
|
||||
path.join(repoRoot, "src", "auto-reply", "reply", "commands-acp"),
|
||||
@@ -41,11 +44,18 @@ const channelIds = [
|
||||
"imessage",
|
||||
"irc",
|
||||
"line",
|
||||
"mattermost",
|
||||
"matrix",
|
||||
"msteams",
|
||||
"nextcloud-talk",
|
||||
"nostr",
|
||||
"qqbot",
|
||||
"signal",
|
||||
"slack",
|
||||
"synology-chat",
|
||||
"telegram",
|
||||
"tlon",
|
||||
"twitch",
|
||||
"web",
|
||||
"whatsapp",
|
||||
"zalo",
|
||||
|
||||
59
scripts/check-tsgo-core-boundary.mjs
Normal file
59
scripts/check-tsgo-core-boundary.mjs
Normal file
@@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "..");
|
||||
const tsgoPath = path.join(repoRoot, "node_modules", ".bin", "tsgo");
|
||||
|
||||
const coreGraphs = [
|
||||
{ name: "core", config: "tsconfig.core.json" },
|
||||
{ name: "core-test", config: "tsconfig.core.test.json" },
|
||||
{ name: "core-test-agents", config: "tsconfig.core.test.agents.json" },
|
||||
{ name: "core-test-non-agents", config: "tsconfig.core.test.non-agents.json" },
|
||||
];
|
||||
|
||||
function normalizeFilePath(filePath) {
|
||||
const normalized = filePath.trim().replaceAll("\\", "/");
|
||||
const normalizedRoot = repoRoot.replaceAll("\\", "/");
|
||||
if (normalized.startsWith(`${normalizedRoot}/`)) {
|
||||
return normalized.slice(normalizedRoot.length + 1);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function listGraphFiles(graph) {
|
||||
const result = spawnSync(tsgoPath, ["-p", graph.config, "--pretty", "false", "--listFilesOnly"], {
|
||||
cwd: repoRoot,
|
||||
encoding: "utf8",
|
||||
maxBuffer: 256 * 1024 * 1024,
|
||||
shell: process.platform === "win32",
|
||||
});
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
if ((result.status ?? 1) !== 0) {
|
||||
const output = [result.stdout, result.stderr].filter(Boolean).join("\n");
|
||||
throw new Error(`${graph.name} file listing failed with exit code ${result.status}\n${output}`);
|
||||
}
|
||||
return (result.stdout ?? "").split(/\r?\n/u).map(normalizeFilePath).filter(Boolean);
|
||||
}
|
||||
|
||||
const violations = [];
|
||||
for (const graph of coreGraphs) {
|
||||
const extensionFiles = listGraphFiles(graph).filter((file) => file.startsWith("extensions/"));
|
||||
for (const file of extensionFiles) {
|
||||
violations.push(`${graph.name}: ${file}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Core tsgo graphs must not include bundled extension files:");
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation}`);
|
||||
}
|
||||
console.error(
|
||||
"Move extension-owned behavior behind plugin SDK contracts, public artifacts, or extension-local tests.",
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -22,6 +22,14 @@ const GRAPH_DEFINITIONS = {
|
||||
config: "tsconfig.core.test.json",
|
||||
description: "core colocated test graph",
|
||||
},
|
||||
"core-test-agents": {
|
||||
config: "tsconfig.core.test.agents.json",
|
||||
description: "core agent colocated test graph",
|
||||
},
|
||||
"core-test-non-agents": {
|
||||
config: "tsconfig.core.test.non-agents.json",
|
||||
description: "core non-agent colocated test graph",
|
||||
},
|
||||
extensions: {
|
||||
config: "tsconfig.extensions.json",
|
||||
description: "bundled extension production graph",
|
||||
|
||||
@@ -305,6 +305,15 @@ function expectAgentGatewayCall(overrides: AgentCallParams): void {
|
||||
expect(agentCall?.params?.threadId).toBe(overrides.threadId);
|
||||
}
|
||||
|
||||
function resolveMatrixRoomTargetForTest(value: string | undefined): string | undefined {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = trimmed.replace(/^(?:matrix:)?(?:channel:|room:)/iu, "").trim();
|
||||
return normalized || undefined;
|
||||
}
|
||||
|
||||
function enableMatrixAcpThreadBindings(): void {
|
||||
replaceSpawnConfig({
|
||||
...hoisted.state.cfg,
|
||||
@@ -318,6 +327,42 @@ function enableMatrixAcpThreadBindings(): void {
|
||||
},
|
||||
},
|
||||
});
|
||||
const matrixPlugin = {
|
||||
messaging: {
|
||||
resolveDeliveryTarget: ({
|
||||
conversationId,
|
||||
parentConversationId,
|
||||
}: {
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
}) => {
|
||||
const parent = resolveMatrixRoomTargetForTest(parentConversationId);
|
||||
const child = conversationId.trim();
|
||||
return parent ? { to: `room:${parent}`, threadId: child } : { to: `room:${child}` };
|
||||
},
|
||||
resolveInboundConversation: ({
|
||||
to,
|
||||
threadId,
|
||||
}: {
|
||||
to?: string;
|
||||
threadId?: string | number;
|
||||
}) => {
|
||||
const parent = resolveMatrixRoomTargetForTest(to);
|
||||
const thread = threadId != null ? String(threadId).trim() : "";
|
||||
return thread && parent
|
||||
? { conversationId: thread, parentConversationId: parent }
|
||||
: parent
|
||||
? { conversationId: parent }
|
||||
: undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
hoisted.getChannelPluginMock.mockImplementation((channelId: string) =>
|
||||
channelId === "matrix" ? matrixPlugin : undefined,
|
||||
);
|
||||
hoisted.getLoadedChannelPluginMock.mockImplementation((channelId: string) =>
|
||||
channelId === "matrix" ? matrixPlugin : undefined,
|
||||
);
|
||||
registerSessionBindingAdapter({
|
||||
channel: "matrix",
|
||||
accountId: "default",
|
||||
@@ -342,6 +387,28 @@ function enableLineCurrentConversationBindings(): void {
|
||||
},
|
||||
},
|
||||
});
|
||||
const linePlugin = {
|
||||
messaging: {
|
||||
resolveInboundConversation: ({
|
||||
conversationId,
|
||||
to,
|
||||
}: {
|
||||
conversationId?: string;
|
||||
to?: string;
|
||||
}) => {
|
||||
const source = (conversationId ?? to ?? "").trim();
|
||||
const normalized =
|
||||
source.match(/^line:(?:(?:user|group|room):)?(.+)$/i)?.[1]?.trim() ?? source;
|
||||
return normalized ? { conversationId: normalized } : undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
hoisted.getChannelPluginMock.mockImplementation((channelId: string) =>
|
||||
channelId === "line" ? linePlugin : undefined,
|
||||
);
|
||||
hoisted.getLoadedChannelPluginMock.mockImplementation((channelId: string) =>
|
||||
channelId === "line" ? linePlugin : undefined,
|
||||
);
|
||||
registerSessionBindingAdapter({
|
||||
channel: "line",
|
||||
accountId: "default",
|
||||
@@ -369,10 +436,45 @@ function enableTelegramCurrentConversationBindings(): void {
|
||||
},
|
||||
},
|
||||
});
|
||||
const telegramPlugin = {
|
||||
messaging: {
|
||||
resolveInboundConversation: ({
|
||||
conversationId,
|
||||
to,
|
||||
threadId,
|
||||
}: {
|
||||
conversationId?: string;
|
||||
to?: string;
|
||||
threadId?: string | number;
|
||||
}) => {
|
||||
const source = (conversationId ?? to ?? "").trim();
|
||||
const normalized = source.replace(/^telegram:(?:group:|channel:|direct:)?/i, "");
|
||||
const explicitThreadId = threadId == null ? "" : String(threadId).trim();
|
||||
if (/^-?\d+$/.test(normalized) && /^\d+$/.test(explicitThreadId)) {
|
||||
return { conversationId: `${normalized}:topic:${explicitThreadId}` };
|
||||
}
|
||||
const topicMatch = /^(-?\d+):topic:(\d+)$/i.exec(normalized);
|
||||
if (topicMatch?.[1] && topicMatch[2]) {
|
||||
return { conversationId: `${topicMatch[1]}:topic:${topicMatch[2]}` };
|
||||
}
|
||||
return /^-?\d+$/.test(normalized) ? { conversationId: normalized } : undefined;
|
||||
},
|
||||
},
|
||||
};
|
||||
hoisted.getChannelPluginMock.mockImplementation((channelId: string) =>
|
||||
channelId === "telegram" ? telegramPlugin : undefined,
|
||||
);
|
||||
hoisted.getLoadedChannelPluginMock.mockImplementation((channelId: string) =>
|
||||
channelId === "telegram" ? telegramPlugin : undefined,
|
||||
);
|
||||
registerSessionBindingAdapter({
|
||||
channel: "telegram",
|
||||
accountId: "default",
|
||||
capabilities: createSessionBindingCapabilities(),
|
||||
capabilities: {
|
||||
bindSupported: true,
|
||||
unbindSupported: true,
|
||||
placements: ["current"] satisfies SessionBindingPlacement[],
|
||||
},
|
||||
bind: async (input) => await hoisted.sessionBindingBindMock(input),
|
||||
listBySession: (targetSessionKey) => hoisted.sessionBindingListBySessionMock(targetSessionKey),
|
||||
resolveByConversation: (ref) => hoisted.sessionBindingResolveByConversationMock(ref),
|
||||
|
||||
@@ -225,65 +225,11 @@ type AcpSpawnBootstrapDeliveryPlan = {
|
||||
};
|
||||
|
||||
function resolvePlacementWithoutChannelPlugin(params: {
|
||||
channel: string;
|
||||
capabilities: { placements: Array<"current" | "child"> };
|
||||
}): "current" | "child" {
|
||||
switch (params.channel) {
|
||||
case "discord":
|
||||
case "matrix":
|
||||
return params.capabilities.placements.includes("child") ? "child" : "current";
|
||||
case "line":
|
||||
case "telegram":
|
||||
return "current";
|
||||
}
|
||||
return params.capabilities.placements.includes("child") ? "child" : "current";
|
||||
}
|
||||
|
||||
function normalizeLineConversationIdFallback(value: string | undefined): string | undefined {
|
||||
const trimmed = normalizeOptionalString(value) ?? "";
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = trimmed.match(/^line:(?:(?:user|group|room):)?(.+)$/i)?.[1]?.trim() ?? trimmed;
|
||||
return normalized ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeTelegramConversationIdFallback(params: {
|
||||
to?: string;
|
||||
threadId?: string | number;
|
||||
groupId?: string;
|
||||
}): string | undefined {
|
||||
const explicitGroupId = normalizeOptionalString(params.groupId);
|
||||
const explicitThreadId =
|
||||
params.threadId != null ? normalizeOptionalString(String(params.threadId)) : undefined;
|
||||
if (
|
||||
explicitGroupId &&
|
||||
explicitThreadId &&
|
||||
/^-?\d+$/.test(explicitGroupId) &&
|
||||
/^\d+$/.test(explicitThreadId)
|
||||
) {
|
||||
return `${explicitGroupId}:topic:${explicitThreadId}`;
|
||||
}
|
||||
|
||||
const trimmed = normalizeOptionalString(params.to) ?? "";
|
||||
if (!trimmed) {
|
||||
return undefined;
|
||||
}
|
||||
const normalized = trimmed.replace(/^telegram:(?:group:|channel:|direct:)?/i, "");
|
||||
const topicMatch = /^(-?\d+):topic:(\d+)$/i.exec(normalized);
|
||||
if (topicMatch?.[1] && topicMatch[2]) {
|
||||
return `${topicMatch[1]}:topic:${topicMatch[2]}`;
|
||||
}
|
||||
return /^-?\d+$/.test(normalized) ? normalized : undefined;
|
||||
}
|
||||
|
||||
const threadBindingFallbackConversationResolvers = {
|
||||
line: (params: { to?: string; groupId?: string }) =>
|
||||
normalizeLineConversationIdFallback(params.groupId ?? params.to),
|
||||
telegram: (params: { to?: string; threadId?: string | number; groupId?: string }) =>
|
||||
normalizeTelegramConversationIdFallback(params),
|
||||
} as const;
|
||||
|
||||
function resolvePluginConversationRefForThreadBinding(params: {
|
||||
channelId: string;
|
||||
to?: string;
|
||||
@@ -559,7 +505,6 @@ function resolveConversationRefForThreadBinding(params: {
|
||||
}): { conversationId: string; parentConversationId?: string } | null {
|
||||
const channel = normalizeOptionalLowercaseString(params.channel);
|
||||
const normalizedChannelId = channel ? normalizeChannelId(channel) : null;
|
||||
const channelKey = normalizedChannelId ?? channel ?? null;
|
||||
const pluginResolvedConversation = normalizedChannelId
|
||||
? resolvePluginConversationRefForThreadBinding({
|
||||
channelId: normalizedChannelId,
|
||||
@@ -571,15 +516,6 @@ function resolveConversationRefForThreadBinding(params: {
|
||||
if (pluginResolvedConversation) {
|
||||
return pluginResolvedConversation;
|
||||
}
|
||||
const compatibilityConversationId =
|
||||
channelKey && Object.hasOwn(threadBindingFallbackConversationResolvers, channelKey)
|
||||
? threadBindingFallbackConversationResolvers[
|
||||
channelKey as keyof typeof threadBindingFallbackConversationResolvers
|
||||
](params)
|
||||
: undefined;
|
||||
if (compatibilityConversationId) {
|
||||
return normalizeConversationTargetRef({ conversationId: compatibilityConversationId });
|
||||
}
|
||||
const parentConversationId = resolveConversationIdFromTargets({
|
||||
targets: [params.to],
|
||||
});
|
||||
@@ -682,7 +618,6 @@ function prepareAcpThreadBinding(params: {
|
||||
const placementToUse =
|
||||
pluginPlacement ??
|
||||
resolvePlacementWithoutChannelPlugin({
|
||||
channel: policy.channel,
|
||||
capabilities,
|
||||
});
|
||||
if (!capabilities.bindSupported || !capabilities.placements.includes(placementToUse)) {
|
||||
|
||||
@@ -99,19 +99,19 @@ import {
|
||||
} from "./plugin-install.js";
|
||||
|
||||
const baseEntry: ChannelPluginCatalogEntry = {
|
||||
id: "zalo",
|
||||
pluginId: "zalo",
|
||||
id: "bundled-chat",
|
||||
pluginId: "bundled-chat",
|
||||
meta: {
|
||||
id: "zalo",
|
||||
label: "Zalo",
|
||||
selectionLabel: "Zalo (Bot API)",
|
||||
docsPath: "/channels/zalo",
|
||||
docsLabel: "zalo",
|
||||
id: "bundled-chat",
|
||||
label: "Bundled Chat",
|
||||
selectionLabel: "Bundled Chat",
|
||||
docsPath: "/channels/bundled-chat",
|
||||
docsLabel: "bundled chat",
|
||||
blurb: "Test",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@openclaw/zalo",
|
||||
localPath: bundledPluginRoot("zalo"),
|
||||
npmSpec: "@openclaw/bundled-chat",
|
||||
localPath: bundledPluginRoot("bundled-chat"),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -132,7 +132,10 @@ beforeEach(() => {
|
||||
function mockRepoLocalPathExists() {
|
||||
vi.mocked(fs.existsSync).mockImplementation((value) => {
|
||||
const raw = String(value);
|
||||
return raw.endsWith(`${path.sep}.git`) || raw.endsWith(`${path.sep}extensions${path.sep}zalo`);
|
||||
return (
|
||||
raw.endsWith(`${path.sep}.git`) ||
|
||||
raw.endsWith(`${path.sep}extensions${path.sep}bundled-chat`)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -157,7 +160,7 @@ async function runInitialValueForChannel(channel: "dev" | "beta") {
|
||||
function expectPluginLoadedFromLocalPath(
|
||||
result: Awaited<ReturnType<typeof ensureChannelSetupPluginInstalled>>,
|
||||
) {
|
||||
const expectedPath = path.resolve(process.cwd(), bundledPluginRoot("zalo"));
|
||||
const expectedPath = path.resolve(process.cwd(), bundledPluginRoot("bundled-chat"));
|
||||
expect(result.installed).toBe(true);
|
||||
expect(result.cfg.plugins?.load?.paths).toContain(expectedPath);
|
||||
}
|
||||
@@ -172,8 +175,8 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
installPluginFromNpmSpec.mockResolvedValue({
|
||||
ok: true,
|
||||
pluginId: "zalo",
|
||||
targetDir: "/tmp/zalo",
|
||||
pluginId: "bundled-chat",
|
||||
targetDir: "/tmp/bundled-chat",
|
||||
extensions: [],
|
||||
});
|
||||
|
||||
@@ -185,13 +188,13 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
});
|
||||
|
||||
expect(result.installed).toBe(true);
|
||||
expect(result.cfg.plugins?.entries?.zalo?.enabled).toBe(true);
|
||||
expect(result.cfg.plugins?.allow).toContain("zalo");
|
||||
expect(result.cfg.plugins?.installs?.zalo?.source).toBe("npm");
|
||||
expect(result.cfg.plugins?.installs?.zalo?.spec).toBe("@openclaw/zalo");
|
||||
expect(result.cfg.plugins?.installs?.zalo?.installPath).toBe("/tmp/zalo");
|
||||
expect(result.cfg.plugins?.entries?.["bundled-chat"]?.enabled).toBe(true);
|
||||
expect(result.cfg.plugins?.allow).toContain("bundled-chat");
|
||||
expect(result.cfg.plugins?.installs?.["bundled-chat"]?.source).toBe("npm");
|
||||
expect(result.cfg.plugins?.installs?.["bundled-chat"]?.spec).toBe("@openclaw/bundled-chat");
|
||||
expect(result.cfg.plugins?.installs?.["bundled-chat"]?.installPath).toBe("/tmp/bundled-chat");
|
||||
expect(installPluginFromNpmSpec).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ spec: "@openclaw/zalo" }),
|
||||
expect.objectContaining({ spec: "@openclaw/bundled-chat" }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -211,7 +214,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
});
|
||||
|
||||
expectPluginLoadedFromLocalPath(result);
|
||||
expect(result.cfg.plugins?.entries?.zalo?.enabled).toBe(true);
|
||||
expect(result.cfg.plugins?.entries?.["bundled-chat"]?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("uses the catalog plugin id for local-path installs", async () => {
|
||||
@@ -226,16 +229,16 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
cfg,
|
||||
entry: {
|
||||
...baseEntry,
|
||||
id: "teams",
|
||||
pluginId: "@openclaw/msteams-plugin",
|
||||
id: "external-chat",
|
||||
pluginId: "@vendor/external-chat-plugin",
|
||||
},
|
||||
prompter,
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(result.installed).toBe(true);
|
||||
expect(result.pluginId).toBe("@openclaw/msteams-plugin");
|
||||
expect(result.cfg.plugins?.entries?.["@openclaw/msteams-plugin"]?.enabled).toBe(true);
|
||||
expect(result.pluginId).toBe("@vendor/external-chat-plugin");
|
||||
expect(result.cfg.plugins?.entries?.["@vendor/external-chat-plugin"]?.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("defaults to local on dev channel when local path exists", async () => {
|
||||
@@ -255,11 +258,11 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
resolveBundledPluginSources.mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
"zalo",
|
||||
"bundled-chat",
|
||||
{
|
||||
pluginId: "zalo",
|
||||
localPath: bundledPluginRootAt("/opt/openclaw", "zalo"),
|
||||
npmSpec: "@openclaw/zalo",
|
||||
pluginId: "bundled-chat",
|
||||
localPath: bundledPluginRootAt("/opt/openclaw", "bundled-chat"),
|
||||
npmSpec: "@openclaw/bundled-chat",
|
||||
},
|
||||
],
|
||||
]),
|
||||
@@ -278,7 +281,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
options: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
value: "local",
|
||||
hint: bundledPluginRootAt("/opt/openclaw", "zalo"),
|
||||
hint: bundledPluginRootAt("/opt/openclaw", "bundled-chat"),
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
@@ -294,11 +297,11 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
resolveBundledPluginSources.mockReturnValue(
|
||||
new Map([
|
||||
[
|
||||
"whatsapp",
|
||||
"bundled-chat",
|
||||
{
|
||||
pluginId: "whatsapp",
|
||||
localPath: bundledPluginRootAt("/opt/openclaw", "whatsapp"),
|
||||
npmSpec: "@openclaw/whatsapp",
|
||||
pluginId: "bundled-chat",
|
||||
localPath: bundledPluginRootAt("/opt/openclaw", "bundled-chat"),
|
||||
npmSpec: "@openclaw/bundled-chat",
|
||||
},
|
||||
],
|
||||
]),
|
||||
@@ -307,16 +310,16 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
await ensureChannelSetupPluginInstalled({
|
||||
cfg,
|
||||
entry: {
|
||||
id: "whatsapp",
|
||||
id: "bundled-chat",
|
||||
meta: {
|
||||
id: "whatsapp",
|
||||
label: "WhatsApp",
|
||||
selectionLabel: "WhatsApp",
|
||||
docsPath: "/channels/whatsapp",
|
||||
id: "bundled-chat",
|
||||
label: "Bundled Chat",
|
||||
selectionLabel: "Bundled Chat",
|
||||
docsPath: "/channels/bundled-chat",
|
||||
blurb: "Test",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@vendor/whatsapp-fork",
|
||||
npmSpec: "@vendor/bundled-chat-fork",
|
||||
},
|
||||
},
|
||||
prompter,
|
||||
@@ -329,7 +332,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
options: [
|
||||
expect.objectContaining({
|
||||
value: "npm",
|
||||
label: "Download from npm (@vendor/whatsapp-fork)",
|
||||
label: "Download from npm (@vendor/bundled-chat-fork)",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
value: "skip",
|
||||
@@ -397,13 +400,13 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {
|
||||
plugins: {},
|
||||
channels: { telegram: { enabled: true } } as never,
|
||||
channels: { "external-chat": { enabled: true } } as never,
|
||||
};
|
||||
const autoEnabledConfig = {
|
||||
...cfg,
|
||||
plugins: {
|
||||
entries: {
|
||||
telegram: { enabled: true },
|
||||
"external-chat": { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
@@ -435,12 +438,12 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
it("scopes channel reloads when setup starts from an empty registry", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
getChannelPluginCatalogEntry.mockReturnValue({ pluginId: "@openclaw/telegram-plugin" });
|
||||
getChannelPluginCatalogEntry.mockReturnValue({ pluginId: "@vendor/external-chat-plugin" });
|
||||
|
||||
reloadChannelSetupPluginRegistryForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
@@ -451,11 +454,11 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
cache: false,
|
||||
onlyPluginIds: ["@openclaw/telegram-plugin"],
|
||||
onlyPluginIds: ["@vendor/external-chat-plugin"],
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
}),
|
||||
);
|
||||
expect(getChannelPluginCatalogEntry).toHaveBeenCalledWith("telegram", {
|
||||
expect(getChannelPluginCatalogEntry).toHaveBeenCalledWith("external-chat", {
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
});
|
||||
@@ -478,7 +481,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
reloadChannelSetupPluginRegistryForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
@@ -492,7 +495,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
it("scopes channel reloads when the global registry is populated but the pinned channel registry is empty", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
getChannelPluginCatalogEntry.mockReturnValue({ pluginId: "@openclaw/telegram-plugin" });
|
||||
getChannelPluginCatalogEntry.mockReturnValue({ pluginId: "@vendor/external-chat-plugin" });
|
||||
const activeRegistry = createEmptyPluginRegistry();
|
||||
activeRegistry.plugins.push(
|
||||
createPluginRecord({
|
||||
@@ -510,7 +513,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
reloadChannelSetupPluginRegistryForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
} finally {
|
||||
@@ -521,7 +524,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
expect.objectContaining({
|
||||
activationSourceConfig: cfg,
|
||||
autoEnabledReasons: {},
|
||||
onlyPluginIds: ["@openclaw/telegram-plugin"],
|
||||
onlyPluginIds: ["@vendor/external-chat-plugin"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -529,12 +532,12 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
it("can load a channel-scoped snapshot without activating the global registry", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
getChannelPluginCatalogEntry.mockReturnValue({ pluginId: "@openclaw/telegram-plugin" });
|
||||
getChannelPluginCatalogEntry.mockReturnValue({ pluginId: "@vendor/external-chat-plugin" });
|
||||
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
@@ -545,12 +548,12 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
cache: false,
|
||||
onlyPluginIds: ["@openclaw/telegram-plugin"],
|
||||
onlyPluginIds: ["@vendor/external-chat-plugin"],
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
activate: false,
|
||||
}),
|
||||
);
|
||||
expect(getChannelPluginCatalogEntry).toHaveBeenCalledWith("telegram", {
|
||||
expect(getChannelPluginCatalogEntry).toHaveBeenCalledWith("external-chat", {
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
});
|
||||
@@ -559,25 +562,25 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
getChannelPluginCatalogEntry
|
||||
.mockReturnValueOnce({ pluginId: "evil-telegram-shadow", origin: "workspace" })
|
||||
.mockReturnValueOnce({ pluginId: "@openclaw/telegram-plugin", origin: "bundled" });
|
||||
.mockReturnValueOnce({ pluginId: "evil-external-chat-shadow", origin: "workspace" })
|
||||
.mockReturnValueOnce({ pluginId: "@vendor/external-chat-plugin", origin: "bundled" });
|
||||
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["@openclaw/telegram-plugin"],
|
||||
onlyPluginIds: ["@vendor/external-chat-plugin"],
|
||||
}),
|
||||
);
|
||||
expect(getChannelPluginCatalogEntry).toHaveBeenNthCalledWith(1, "telegram", {
|
||||
expect(getChannelPluginCatalogEntry).toHaveBeenNthCalledWith(1, "external-chat", {
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
expect(getChannelPluginCatalogEntry).toHaveBeenNthCalledWith(2, "telegram", {
|
||||
expect(getChannelPluginCatalogEntry).toHaveBeenNthCalledWith(2, "external-chat", {
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
excludeWorkspace: true,
|
||||
});
|
||||
@@ -588,24 +591,24 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: ["trusted-telegram-shadow"],
|
||||
allow: ["trusted-external-chat-shadow"],
|
||||
},
|
||||
};
|
||||
getChannelPluginCatalogEntry.mockReturnValue({
|
||||
pluginId: "trusted-telegram-shadow",
|
||||
pluginId: "trusted-external-chat-shadow",
|
||||
origin: "workspace",
|
||||
});
|
||||
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["trusted-telegram-shadow"],
|
||||
onlyPluginIds: ["trusted-external-chat-shadow"],
|
||||
}),
|
||||
);
|
||||
expect(getChannelPluginCatalogEntry).toHaveBeenCalledTimes(1);
|
||||
@@ -618,7 +621,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
@@ -633,14 +636,14 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {};
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [{ id: "custom-telegram-plugin", channels: ["telegram"] }],
|
||||
plugins: [{ id: "custom-external-chat-plugin", channels: ["external-chat"] }],
|
||||
diagnostics: [],
|
||||
});
|
||||
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
@@ -651,7 +654,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
cache: false,
|
||||
onlyPluginIds: ["custom-telegram-plugin"],
|
||||
onlyPluginIds: ["custom-external-chat-plugin"],
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
activate: false,
|
||||
}),
|
||||
@@ -664,10 +667,10 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "custom-telegram-plugin",
|
||||
id: "custom-external-chat-plugin",
|
||||
channels: [],
|
||||
activation: {
|
||||
onChannels: ["telegram"],
|
||||
onChannels: ["external-chat"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -677,13 +680,13 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
onlyPluginIds: ["custom-telegram-plugin"],
|
||||
onlyPluginIds: ["custom-external-chat-plugin"],
|
||||
}),
|
||||
);
|
||||
expect(loadPluginManifestRegistry).toHaveBeenCalledWith(
|
||||
@@ -699,10 +702,10 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "custom-telegram-plugin",
|
||||
id: "custom-external-chat-plugin",
|
||||
channels: [],
|
||||
activation: {
|
||||
onChannels: ["telegram"],
|
||||
onChannels: ["external-chat"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -712,7 +715,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
@@ -730,11 +733,11 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "evil-telegram-shadow",
|
||||
id: "evil-external-chat-shadow",
|
||||
channels: [],
|
||||
origin: "workspace",
|
||||
activation: {
|
||||
onChannels: ["telegram"],
|
||||
onChannels: ["external-chat"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -744,13 +747,13 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
onlyPluginIds: ["evil-telegram-shadow"],
|
||||
onlyPluginIds: ["evil-external-chat-shadow"],
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
@@ -769,11 +772,11 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "custom-telegram-plugin",
|
||||
id: "custom-external-chat-plugin",
|
||||
channels: [],
|
||||
origin: "bundled",
|
||||
activation: {
|
||||
onChannels: ["telegram"],
|
||||
onChannels: ["external-chat"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -783,13 +786,13 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
onlyPluginIds: ["custom-telegram-plugin"],
|
||||
onlyPluginIds: ["custom-external-chat-plugin"],
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
@@ -802,17 +805,17 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
const runtime = makeRuntime();
|
||||
const cfg: OpenClawConfig = {
|
||||
plugins: {
|
||||
deny: ["custom-telegram-plugin"],
|
||||
deny: ["custom-external-chat-plugin"],
|
||||
},
|
||||
};
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "custom-telegram-plugin",
|
||||
id: "custom-external-chat-plugin",
|
||||
channels: [],
|
||||
origin: "bundled",
|
||||
activation: {
|
||||
onChannels: ["telegram"],
|
||||
onChannels: ["external-chat"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -822,13 +825,13 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
onlyPluginIds: ["custom-telegram-plugin"],
|
||||
onlyPluginIds: ["custom-external-chat-plugin"],
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
@@ -842,20 +845,20 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: ["evil-telegram-shadow"],
|
||||
allow: ["evil-external-chat-shadow"],
|
||||
entries: {
|
||||
"evil-telegram-shadow": { enabled: false },
|
||||
"evil-external-chat-shadow": { enabled: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "evil-telegram-shadow",
|
||||
id: "evil-external-chat-shadow",
|
||||
channels: [],
|
||||
origin: "workspace",
|
||||
activation: {
|
||||
onChannels: ["telegram"],
|
||||
onChannels: ["external-chat"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -865,13 +868,13 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
onlyPluginIds: ["evil-telegram-shadow"],
|
||||
onlyPluginIds: ["evil-external-chat-shadow"],
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
@@ -885,18 +888,18 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
const cfg: OpenClawConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
"custom-telegram-plugin": { enabled: false },
|
||||
"custom-external-chat-plugin": { enabled: false },
|
||||
},
|
||||
},
|
||||
};
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "custom-telegram-plugin",
|
||||
id: "custom-external-chat-plugin",
|
||||
channels: [],
|
||||
origin: "bundled",
|
||||
activation: {
|
||||
onChannels: ["telegram"],
|
||||
onChannels: ["external-chat"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -906,13 +909,13 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
onlyPluginIds: ["custom-telegram-plugin"],
|
||||
onlyPluginIds: ["custom-external-chat-plugin"],
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
@@ -927,11 +930,11 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadPluginManifestRegistry.mockReturnValue({
|
||||
plugins: [
|
||||
{
|
||||
id: "custom-telegram-global",
|
||||
id: "custom-external-chat-global",
|
||||
channels: [],
|
||||
origin: "global",
|
||||
activation: {
|
||||
onChannels: ["telegram"],
|
||||
onChannels: ["external-chat"],
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -941,13 +944,13 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
expect(loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
onlyPluginIds: ["custom-telegram-global"],
|
||||
onlyPluginIds: ["custom-external-chat-global"],
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
@@ -963,8 +966,8 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel({
|
||||
cfg,
|
||||
runtime,
|
||||
channel: "msteams",
|
||||
pluginId: "@openclaw/msteams-plugin",
|
||||
channel: "external-chat",
|
||||
pluginId: "@vendor/external-chat-plugin",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
});
|
||||
|
||||
@@ -975,7 +978,7 @@ describe("ensureChannelSetupPluginInstalled", () => {
|
||||
autoEnabledReasons: {},
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
cache: false,
|
||||
onlyPluginIds: ["@openclaw/msteams-plugin"],
|
||||
onlyPluginIds: ["@vendor/external-chat-plugin"],
|
||||
includeSetupOnlyChannelPlugins: true,
|
||||
activate: false,
|
||||
}),
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
ensureChannelSetupPluginInstalled,
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel,
|
||||
} from "./channel-setup/plugin-install.js";
|
||||
import { configMocks, offsetMocks } from "./channels.mock-harness.js";
|
||||
import { configMocks, lifecycleMocks } from "./channels.mock-harness.js";
|
||||
import {
|
||||
createMSTeamsCatalogEntry,
|
||||
createMSTeamsSetupPlugin,
|
||||
createExternalChatCatalogEntry,
|
||||
createExternalChatSetupPlugin,
|
||||
} from "./channels.plugin-install.test-helpers.js";
|
||||
import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js";
|
||||
|
||||
@@ -53,66 +53,66 @@ vi.mock("./channel-setup/plugin-install.js", () => pluginInstallMocks);
|
||||
const runtime = createTestRuntime();
|
||||
|
||||
function listConfiguredAccountIds(
|
||||
channelConfig: { accounts?: Record<string, unknown>; botToken?: string } | undefined,
|
||||
channelConfig: { accounts?: Record<string, unknown>; token?: string } | undefined,
|
||||
): string[] {
|
||||
const accountIds = Object.keys(channelConfig?.accounts ?? {});
|
||||
if (accountIds.length > 0) {
|
||||
return accountIds;
|
||||
}
|
||||
if (channelConfig?.botToken) {
|
||||
if (channelConfig?.token) {
|
||||
return [DEFAULT_ACCOUNT_ID];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function createTelegramAddTestPlugin(): ChannelPlugin {
|
||||
const resolveTelegramAccount = (
|
||||
function createLifecycleChatAddTestPlugin(): ChannelPlugin {
|
||||
const resolveLifecycleChatAccount = (
|
||||
cfg: Parameters<NonNullable<ChannelPlugin["config"]["resolveAccount"]>>[0],
|
||||
accountId: string,
|
||||
) => {
|
||||
const telegram = cfg.channels?.telegram as
|
||||
const lifecycleChat = cfg.channels?.["lifecycle-chat"] as
|
||||
| {
|
||||
botToken?: string;
|
||||
token?: string;
|
||||
enabled?: boolean;
|
||||
accounts?: Record<string, { botToken?: string; enabled?: boolean }>;
|
||||
accounts?: Record<string, { token?: string; enabled?: boolean }>;
|
||||
}
|
||||
| undefined;
|
||||
const resolvedAccountId = accountId || DEFAULT_ACCOUNT_ID;
|
||||
const scoped = telegram?.accounts?.[resolvedAccountId];
|
||||
const scoped = lifecycleChat?.accounts?.[resolvedAccountId];
|
||||
return {
|
||||
token: scoped?.botToken ?? telegram?.botToken ?? "",
|
||||
token: scoped?.token ?? lifecycleChat?.token ?? "",
|
||||
enabled:
|
||||
typeof scoped?.enabled === "boolean"
|
||||
? scoped.enabled
|
||||
: typeof telegram?.enabled === "boolean"
|
||||
? telegram.enabled
|
||||
: typeof lifecycleChat?.enabled === "boolean"
|
||||
? lifecycleChat.enabled
|
||||
: true,
|
||||
};
|
||||
};
|
||||
|
||||
return {
|
||||
...createChannelTestPluginBase({
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
docsPath: "/channels/telegram",
|
||||
id: "lifecycle-chat",
|
||||
label: "Lifecycle Chat",
|
||||
docsPath: "/channels/lifecycle-chat",
|
||||
}),
|
||||
config: {
|
||||
listAccountIds: (cfg) =>
|
||||
listConfiguredAccountIds(
|
||||
cfg.channels?.telegram as
|
||||
| { accounts?: Record<string, unknown>; botToken?: string }
|
||||
cfg.channels?.["lifecycle-chat"] as
|
||||
| { accounts?: Record<string, unknown>; token?: string }
|
||||
| undefined,
|
||||
),
|
||||
resolveAccount: resolveTelegramAccount,
|
||||
resolveAccount: resolveLifecycleChatAccount,
|
||||
},
|
||||
setup: {
|
||||
resolveAccountId: ({ accountId }) => accountId || DEFAULT_ACCOUNT_ID,
|
||||
applyAccountConfig: ({ cfg, accountId, input }) => {
|
||||
const telegram = (cfg.channels?.telegram as
|
||||
const lifecycleChat = (cfg.channels?.["lifecycle-chat"] as
|
||||
| {
|
||||
enabled?: boolean;
|
||||
botToken?: string;
|
||||
accounts?: Record<string, { botToken?: string }>;
|
||||
token?: string;
|
||||
accounts?: Record<string, { token?: string }>;
|
||||
}
|
||||
| undefined) ?? { enabled: true };
|
||||
const resolvedAccountId = accountId || DEFAULT_ACCOUNT_ID;
|
||||
@@ -121,10 +121,10 @@ function createTelegramAddTestPlugin(): ChannelPlugin {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
telegram: {
|
||||
...telegram,
|
||||
"lifecycle-chat": {
|
||||
...lifecycleChat,
|
||||
enabled: true,
|
||||
...(input.token ? { botToken: input.token } : {}),
|
||||
...(input.token ? { token: input.token } : {}),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -133,14 +133,14 @@ function createTelegramAddTestPlugin(): ChannelPlugin {
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
telegram: {
|
||||
...telegram,
|
||||
"lifecycle-chat": {
|
||||
...lifecycleChat,
|
||||
enabled: true,
|
||||
accounts: {
|
||||
...telegram.accounts,
|
||||
...lifecycleChat.accounts,
|
||||
[resolvedAccountId]: {
|
||||
...telegram.accounts?.[resolvedAccountId],
|
||||
...(input.token ? { botToken: input.token } : {}),
|
||||
...lifecycleChat.accounts?.[resolvedAccountId],
|
||||
...(input.token ? { token: input.token } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -150,10 +150,10 @@ function createTelegramAddTestPlugin(): ChannelPlugin {
|
||||
},
|
||||
lifecycle: {
|
||||
onAccountConfigChanged: async ({ prevCfg, nextCfg, accountId }) => {
|
||||
const prevTelegram = resolveTelegramAccount(prevCfg, accountId) as { token?: string };
|
||||
const nextTelegram = resolveTelegramAccount(nextCfg, accountId) as { token?: string };
|
||||
if ((prevTelegram.token ?? "").trim() !== (nextTelegram.token ?? "").trim()) {
|
||||
await offsetMocks.deleteTelegramUpdateOffset({ accountId });
|
||||
const prev = resolveLifecycleChatAccount(prevCfg, accountId) as { token?: string };
|
||||
const next = resolveLifecycleChatAccount(nextCfg, accountId) as { token?: string };
|
||||
if ((prev.token ?? "").trim() !== (next.token ?? "").trim()) {
|
||||
await lifecycleMocks.onAccountConfigChanged({ accountId });
|
||||
}
|
||||
},
|
||||
},
|
||||
@@ -164,17 +164,17 @@ function setMinimalChannelsAddRegistryForTests(): void {
|
||||
setActivePluginRegistry(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "telegram",
|
||||
plugin: createTelegramAddTestPlugin(),
|
||||
pluginId: "lifecycle-chat",
|
||||
plugin: createLifecycleChatAddTestPlugin(),
|
||||
source: "test",
|
||||
},
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function registerMSTeamsSetupPlugin(pluginId = "@openclaw/msteams-plugin"): void {
|
||||
function registerExternalChatSetupPlugin(pluginId = "@vendor/external-chat-plugin"): void {
|
||||
vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel).mockReturnValue(
|
||||
createTestRegistry([{ pluginId, plugin: createMSTeamsSetupPlugin(), source: "test" }]),
|
||||
createTestRegistry([{ pluginId, plugin: createExternalChatSetupPlugin(), source: "test" }]),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -235,7 +235,7 @@ describe("channelsAddCommand", () => {
|
||||
.mockImplementation(async (params: { nextConfig: unknown }) => {
|
||||
await configMocks.writeConfigFile(params.nextConfig);
|
||||
});
|
||||
offsetMocks.deleteTelegramUpdateOffset.mockClear();
|
||||
lifecycleMocks.onAccountConfigChanged.mockClear();
|
||||
runtime.log.mockClear();
|
||||
runtime.error.mockClear();
|
||||
runtime.exit.mockClear();
|
||||
@@ -255,54 +255,54 @@ describe("channelsAddCommand", () => {
|
||||
setMinimalChannelsAddRegistryForTests();
|
||||
});
|
||||
|
||||
it("clears telegram update offsets only when the token changes", async () => {
|
||||
it("runs channel lifecycle hooks only when account config changes", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {
|
||||
channels: {
|
||||
telegram: { botToken: "old-token", enabled: true },
|
||||
"lifecycle-chat": { token: "old-token", enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await channelsAddCommand(
|
||||
{ channel: "telegram", account: "default", token: "new-token" },
|
||||
{ channel: "lifecycle-chat", account: "default", token: "new-token" },
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
|
||||
expect(offsetMocks.deleteTelegramUpdateOffset).toHaveBeenCalledTimes(1);
|
||||
expect(offsetMocks.deleteTelegramUpdateOffset).toHaveBeenCalledWith({ accountId: "default" });
|
||||
expect(lifecycleMocks.onAccountConfigChanged).toHaveBeenCalledTimes(1);
|
||||
expect(lifecycleMocks.onAccountConfigChanged).toHaveBeenCalledWith({ accountId: "default" });
|
||||
|
||||
offsetMocks.deleteTelegramUpdateOffset.mockClear();
|
||||
lifecycleMocks.onAccountConfigChanged.mockClear();
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({
|
||||
...baseConfigSnapshot,
|
||||
config: {
|
||||
channels: {
|
||||
telegram: { botToken: "same-token", enabled: true },
|
||||
"lifecycle-chat": { token: "same-token", enabled: true },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await channelsAddCommand(
|
||||
{ channel: "telegram", account: "default", token: "same-token" },
|
||||
{ channel: "lifecycle-chat", account: "default", token: "same-token" },
|
||||
runtime,
|
||||
{ hasFlags: true },
|
||||
);
|
||||
|
||||
expect(offsetMocks.deleteTelegramUpdateOffset).not.toHaveBeenCalled();
|
||||
expect(lifecycleMocks.onAccountConfigChanged).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("loads external channel setup snapshots for newly installed and existing plugins", async () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseConfigSnapshot });
|
||||
setActivePluginRegistry(createTestRegistry());
|
||||
const catalogEntry = createMSTeamsCatalogEntry();
|
||||
const catalogEntry = createExternalChatCatalogEntry();
|
||||
catalogMocks.listChannelPluginCatalogEntries.mockReturnValue([catalogEntry]);
|
||||
registerMSTeamsSetupPlugin("msteams");
|
||||
registerExternalChatSetupPlugin("external-chat");
|
||||
|
||||
await channelsAddCommand(
|
||||
{
|
||||
channel: "msteams",
|
||||
channel: "external-chat",
|
||||
account: "default",
|
||||
token: "tenant-scoped",
|
||||
},
|
||||
@@ -317,7 +317,7 @@ describe("channelsAddCommand", () => {
|
||||
expect(configMocks.writeConfigFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channels: {
|
||||
msteams: expect.objectContaining({
|
||||
"external-chat": expect.objectContaining({
|
||||
enabled: true,
|
||||
}),
|
||||
},
|
||||
@@ -333,7 +333,7 @@ describe("channelsAddCommand", () => {
|
||||
|
||||
await channelsAddCommand(
|
||||
{
|
||||
channel: "msteams",
|
||||
channel: "external-chat",
|
||||
account: "default",
|
||||
token: "tenant-installed",
|
||||
},
|
||||
@@ -346,7 +346,7 @@ describe("channelsAddCommand", () => {
|
||||
expect(configMocks.writeConfigFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channels: {
|
||||
msteams: expect.objectContaining({
|
||||
"external-chat": expect.objectContaining({
|
||||
enabled: true,
|
||||
}),
|
||||
},
|
||||
@@ -358,43 +358,43 @@ describe("channelsAddCommand", () => {
|
||||
configMocks.readConfigFileSnapshot.mockResolvedValue({ ...baseConfigSnapshot });
|
||||
setActivePluginRegistry(createTestRegistry());
|
||||
const catalogEntry: ChannelPluginCatalogEntry = {
|
||||
id: "msteams",
|
||||
pluginId: "@openclaw/msteams-plugin",
|
||||
id: "external-chat",
|
||||
pluginId: "@vendor/external-chat-plugin",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "teams channel",
|
||||
id: "external-chat",
|
||||
label: "External Chat",
|
||||
selectionLabel: "External Chat",
|
||||
docsPath: "/channels/external-chat",
|
||||
blurb: "external chat channel",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@openclaw/msteams",
|
||||
npmSpec: "@vendor/external-chat",
|
||||
},
|
||||
};
|
||||
catalogMocks.listChannelPluginCatalogEntries.mockReturnValue([catalogEntry]);
|
||||
vi.mocked(ensureChannelSetupPluginInstalled).mockImplementation(async ({ cfg }) => ({
|
||||
cfg,
|
||||
installed: true,
|
||||
pluginId: "@vendor/teams-runtime",
|
||||
pluginId: "@vendor/external-chat-runtime",
|
||||
}));
|
||||
vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel).mockReturnValue(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "@vendor/teams-runtime",
|
||||
pluginId: "@vendor/external-chat-runtime",
|
||||
plugin: {
|
||||
...createChannelTestPluginBase({
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
docsPath: "/channels/msteams",
|
||||
id: "external-chat",
|
||||
label: "External Chat",
|
||||
docsPath: "/channels/external-chat",
|
||||
}),
|
||||
setup: {
|
||||
applyAccountConfig: vi.fn(({ cfg, input }) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
msteams: {
|
||||
"external-chat": {
|
||||
enabled: true,
|
||||
tenantId: input.token,
|
||||
token: input.token,
|
||||
},
|
||||
},
|
||||
})),
|
||||
@@ -407,7 +407,7 @@ describe("channelsAddCommand", () => {
|
||||
|
||||
await channelsAddCommand(
|
||||
{
|
||||
channel: "msteams",
|
||||
channel: "external-chat",
|
||||
account: "default",
|
||||
token: "tenant-scoped",
|
||||
},
|
||||
@@ -419,7 +419,7 @@ describe("channelsAddCommand", () => {
|
||||
expect(configMocks.writeConfigFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channels: {
|
||||
msteams: expect.objectContaining({
|
||||
"external-chat": expect.objectContaining({
|
||||
enabled: true,
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -27,6 +27,12 @@ export const offsetMocks: {
|
||||
deleteTelegramUpdateOffset: vi.fn().mockResolvedValue(undefined) as unknown as MockFn,
|
||||
};
|
||||
|
||||
export const lifecycleMocks: {
|
||||
onAccountConfigChanged: MockFn;
|
||||
} = {
|
||||
onAccountConfigChanged: vi.fn().mockResolvedValue(undefined) as unknown as MockFn,
|
||||
};
|
||||
|
||||
export const secretMocks = {
|
||||
resolveCommandConfigWithSecrets: vi.fn(async ({ config }: { config: unknown }) => ({
|
||||
resolvedConfig: config,
|
||||
|
||||
@@ -14,38 +14,38 @@ export function createMockChannelSetupPluginInstallModule(
|
||||
};
|
||||
}
|
||||
|
||||
export function createMSTeamsCatalogEntry(): ChannelPluginCatalogEntry {
|
||||
export function createExternalChatCatalogEntry(): ChannelPluginCatalogEntry {
|
||||
return {
|
||||
id: "msteams",
|
||||
pluginId: "@openclaw/msteams-plugin",
|
||||
id: "external-chat",
|
||||
pluginId: "@vendor/external-chat-plugin",
|
||||
meta: {
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
selectionLabel: "Microsoft Teams",
|
||||
docsPath: "/channels/msteams",
|
||||
blurb: "teams channel",
|
||||
id: "external-chat",
|
||||
label: "External Chat",
|
||||
selectionLabel: "External Chat",
|
||||
docsPath: "/channels/external-chat",
|
||||
blurb: "external chat channel",
|
||||
},
|
||||
install: {
|
||||
npmSpec: "@openclaw/msteams",
|
||||
npmSpec: "@vendor/external-chat",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createMSTeamsSetupPlugin(): ChannelPlugin {
|
||||
export function createExternalChatSetupPlugin(): ChannelPlugin {
|
||||
return {
|
||||
...createChannelTestPluginBase({
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
docsPath: "/channels/msteams",
|
||||
id: "external-chat",
|
||||
label: "External Chat",
|
||||
docsPath: "/channels/external-chat",
|
||||
}),
|
||||
setup: {
|
||||
applyAccountConfig: vi.fn(({ cfg, input }) => ({
|
||||
...cfg,
|
||||
channels: {
|
||||
...cfg.channels,
|
||||
msteams: {
|
||||
"external-chat": {
|
||||
enabled: true,
|
||||
tenantId: input.token,
|
||||
token: input.token,
|
||||
},
|
||||
},
|
||||
})),
|
||||
@@ -53,23 +53,23 @@ export function createMSTeamsSetupPlugin(): ChannelPlugin {
|
||||
} as ChannelPlugin;
|
||||
}
|
||||
|
||||
export function createMSTeamsDeletePlugin(): ChannelPlugin {
|
||||
export function createExternalChatDeletePlugin(): ChannelPlugin {
|
||||
return {
|
||||
...createChannelTestPluginBase({
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
docsPath: "/channels/msteams",
|
||||
id: "external-chat",
|
||||
label: "External Chat",
|
||||
docsPath: "/channels/external-chat",
|
||||
}),
|
||||
config: {
|
||||
...createChannelTestPluginBase({
|
||||
id: "msteams",
|
||||
label: "Microsoft Teams",
|
||||
docsPath: "/channels/msteams",
|
||||
id: "external-chat",
|
||||
label: "External Chat",
|
||||
docsPath: "/channels/external-chat",
|
||||
}).config,
|
||||
deleteAccount: vi.fn(({ cfg }: { cfg: Record<string, unknown> }) => {
|
||||
const channels = (cfg.channels as Record<string, unknown> | undefined) ?? {};
|
||||
const nextChannels = { ...channels };
|
||||
delete nextChannels.msteams;
|
||||
delete nextChannels["external-chat"];
|
||||
return {
|
||||
...cfg,
|
||||
channels: nextChannels as ChannelsConfig,
|
||||
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
} from "./channel-setup/plugin-install.js";
|
||||
import { configMocks } from "./channels.mock-harness.js";
|
||||
import {
|
||||
createMSTeamsCatalogEntry,
|
||||
createMSTeamsDeletePlugin,
|
||||
createExternalChatCatalogEntry,
|
||||
createExternalChatDeletePlugin,
|
||||
} from "./channels.plugin-install.test-helpers.js";
|
||||
import { baseConfigSnapshot, createTestRuntime } from "./test-runtime-config-helpers.js";
|
||||
|
||||
@@ -86,22 +86,22 @@ describe("channelsRemoveCommand", () => {
|
||||
...baseConfigSnapshot,
|
||||
config: {
|
||||
channels: {
|
||||
msteams: {
|
||||
"external-chat": {
|
||||
enabled: true,
|
||||
tenantId: "tenant-1",
|
||||
token: "token-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const catalogEntry: ChannelPluginCatalogEntry = createMSTeamsCatalogEntry();
|
||||
const catalogEntry: ChannelPluginCatalogEntry = createExternalChatCatalogEntry();
|
||||
catalogMocks.listChannelPluginCatalogEntries.mockReturnValue([catalogEntry]);
|
||||
const scopedPlugin = createMSTeamsDeletePlugin();
|
||||
const scopedPlugin = createExternalChatDeletePlugin();
|
||||
vi.mocked(loadChannelSetupPluginRegistrySnapshotForChannel)
|
||||
.mockReturnValueOnce(createTestRegistry())
|
||||
.mockReturnValueOnce(
|
||||
createTestRegistry([
|
||||
{
|
||||
pluginId: "@openclaw/msteams-plugin",
|
||||
pluginId: "@vendor/external-chat-plugin",
|
||||
plugin: scopedPlugin,
|
||||
source: "test",
|
||||
},
|
||||
@@ -110,7 +110,7 @@ describe("channelsRemoveCommand", () => {
|
||||
|
||||
await channelsRemoveCommand(
|
||||
{
|
||||
channel: "msteams",
|
||||
channel: "external-chat",
|
||||
account: "default",
|
||||
delete: true,
|
||||
},
|
||||
@@ -125,7 +125,7 @@ describe("channelsRemoveCommand", () => {
|
||||
expect(configMocks.writeConfigFile).toHaveBeenCalledWith(
|
||||
expect.not.objectContaining({
|
||||
channels: expect.objectContaining({
|
||||
msteams: expect.anything(),
|
||||
"external-chat": expect.anything(),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -190,8 +190,8 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
resolveDefaultAgentId.mockReturnValue("default");
|
||||
listTrustedChannelPluginCatalogEntries.mockReturnValue([
|
||||
{
|
||||
id: "telegram",
|
||||
pluginId: "@openclaw/telegram-plugin",
|
||||
id: "external-chat",
|
||||
pluginId: "@vendor/external-chat-plugin",
|
||||
origin: "bundled",
|
||||
},
|
||||
]);
|
||||
@@ -234,8 +234,8 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
);
|
||||
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
pluginId: "@openclaw/telegram-plugin",
|
||||
channel: "external-chat",
|
||||
pluginId: "@vendor/external-chat-plugin",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
}),
|
||||
);
|
||||
@@ -243,14 +243,14 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
|
||||
it("keeps trusted workspace overrides eligible during preload", async () => {
|
||||
listTrustedChannelPluginCatalogEntries.mockReturnValue([
|
||||
{ id: "telegram", pluginId: "trusted-telegram-shadow", origin: "workspace" },
|
||||
{ id: "external-chat", pluginId: "trusted-external-chat-shadow", origin: "workspace" },
|
||||
]);
|
||||
|
||||
await setupChannels(
|
||||
{
|
||||
plugins: {
|
||||
enabled: true,
|
||||
allow: ["trusted-telegram-shadow"],
|
||||
allow: ["trusted-external-chat-shadow"],
|
||||
},
|
||||
} as never,
|
||||
{} as never,
|
||||
@@ -262,8 +262,8 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
|
||||
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
pluginId: "trusted-telegram-shadow",
|
||||
channel: "external-chat",
|
||||
pluginId: "trusted-external-chat-shadow",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
}),
|
||||
);
|
||||
@@ -273,8 +273,8 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
resolveChannelSetupEntries.mockReturnValue({
|
||||
entries: [
|
||||
{
|
||||
id: "telegram",
|
||||
meta: makeMeta("telegram", "Telegram"),
|
||||
id: "external-chat",
|
||||
meta: makeMeta("external-chat", "External Chat"),
|
||||
},
|
||||
],
|
||||
installedCatalogEntries: [],
|
||||
@@ -413,52 +413,52 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
cfg: {
|
||||
...cfg,
|
||||
channels: {
|
||||
telegram: { token: "secret" },
|
||||
"external-chat": { token: "secret" },
|
||||
},
|
||||
} as never,
|
||||
}));
|
||||
const setupWizard = {
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
getStatus: vi.fn(async () => ({
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
configured: false,
|
||||
statusLines: [],
|
||||
})),
|
||||
configure,
|
||||
} as ChannelSetupPlugin["setupWizard"];
|
||||
const telegramPlugin = makeSetupPlugin({
|
||||
id: "telegram",
|
||||
label: "Telegram",
|
||||
const externalChatPlugin = makeSetupPlugin({
|
||||
id: "external-chat",
|
||||
label: "External Chat",
|
||||
setupWizard,
|
||||
});
|
||||
const installedCatalogEntry = makeCatalogEntry("telegram", "Telegram", {
|
||||
pluginId: "telegram",
|
||||
const installedCatalogEntry = makeCatalogEntry("external-chat", "External Chat", {
|
||||
pluginId: "external-chat",
|
||||
origin: "bundled",
|
||||
});
|
||||
resolveChannelSetupEntries.mockReturnValue({
|
||||
entries: [
|
||||
{
|
||||
id: "telegram",
|
||||
meta: makeMeta("telegram", "Telegram"),
|
||||
id: "external-chat",
|
||||
meta: makeMeta("external-chat", "External Chat"),
|
||||
},
|
||||
],
|
||||
installedCatalogEntries: [installedCatalogEntry],
|
||||
installableCatalogEntries: [],
|
||||
installedCatalogById: new Map([["telegram", installedCatalogEntry]]),
|
||||
installedCatalogById: new Map([["external-chat", installedCatalogEntry]]),
|
||||
installableCatalogById: new Map(),
|
||||
});
|
||||
loadChannelSetupPluginRegistrySnapshotForChannel.mockReturnValue(
|
||||
makePluginRegistry({
|
||||
channels: [
|
||||
{
|
||||
pluginId: "telegram",
|
||||
pluginId: "external-chat",
|
||||
source: "bundled",
|
||||
plugin: telegramPlugin,
|
||||
plugin: externalChatPlugin,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const select = vi.fn().mockResolvedValueOnce("telegram").mockResolvedValueOnce("__done__");
|
||||
const select = vi.fn().mockResolvedValueOnce("external-chat").mockResolvedValueOnce("__done__");
|
||||
|
||||
const next = await setupChannels(
|
||||
{} as never,
|
||||
@@ -478,8 +478,8 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledTimes(1);
|
||||
expect(loadChannelSetupPluginRegistrySnapshotForChannel).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "telegram",
|
||||
pluginId: "telegram",
|
||||
channel: "external-chat",
|
||||
pluginId: "external-chat",
|
||||
workspaceDir: "/tmp/openclaw-workspace",
|
||||
}),
|
||||
);
|
||||
@@ -492,16 +492,16 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
);
|
||||
expect(next).toEqual({
|
||||
channels: {
|
||||
telegram: { token: "secret" },
|
||||
"external-chat": { token: "secret" },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not load or re-enable an explicitly disabled channel when selected lazily", async () => {
|
||||
const setupWizard = {
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
getStatus: vi.fn(async () => ({
|
||||
channel: "telegram",
|
||||
channel: "external-chat",
|
||||
configured: true,
|
||||
statusLines: [],
|
||||
})),
|
||||
@@ -510,8 +510,8 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
resolveChannelSetupEntries.mockReturnValue({
|
||||
entries: [
|
||||
{
|
||||
id: "telegram",
|
||||
meta: makeMeta("telegram", "Telegram"),
|
||||
id: "external-chat",
|
||||
meta: makeMeta("external-chat", "External Chat"),
|
||||
},
|
||||
],
|
||||
installedCatalogEntries: [],
|
||||
@@ -519,11 +519,11 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
installedCatalogById: new Map(),
|
||||
installableCatalogById: new Map(),
|
||||
});
|
||||
const select = vi.fn().mockResolvedValueOnce("telegram").mockResolvedValueOnce("__done__");
|
||||
const select = vi.fn().mockResolvedValueOnce("external-chat").mockResolvedValueOnce("__done__");
|
||||
const note = vi.fn(async () => undefined);
|
||||
const cfg = {
|
||||
channels: {
|
||||
telegram: { enabled: false, token: "secret" },
|
||||
"external-chat": { enabled: false, token: "secret" },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -544,13 +544,13 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
|
||||
expect(loadChannelSetupPluginRegistrySnapshotForChannel).not.toHaveBeenCalled();
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"telegram cannot be configured while disabled. Enable it before setup.",
|
||||
"external-chat cannot be configured while disabled. Enable it before setup.",
|
||||
"Channel setup",
|
||||
);
|
||||
expect(setupWizard.configure).not.toHaveBeenCalled();
|
||||
expect(next).toEqual({
|
||||
channels: {
|
||||
telegram: { enabled: false, token: "secret" },
|
||||
"external-chat": { enabled: false, token: "secret" },
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -559,8 +559,8 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
resolveChannelSetupEntries.mockReturnValue({
|
||||
entries: [
|
||||
{
|
||||
id: "telegram",
|
||||
meta: makeMeta("telegram", "Telegram"),
|
||||
id: "external-chat",
|
||||
meta: makeMeta("external-chat", "External Chat"),
|
||||
},
|
||||
],
|
||||
installedCatalogEntries: [],
|
||||
@@ -568,12 +568,12 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
installedCatalogById: new Map(),
|
||||
installableCatalogById: new Map(),
|
||||
});
|
||||
const select = vi.fn().mockResolvedValueOnce("telegram").mockResolvedValueOnce("__done__");
|
||||
const select = vi.fn().mockResolvedValueOnce("external-chat").mockResolvedValueOnce("__done__");
|
||||
const note = vi.fn(async () => undefined);
|
||||
const cfg = {
|
||||
plugins: { enabled: false },
|
||||
channels: {
|
||||
telegram: { enabled: true, token: "secret" },
|
||||
"external-chat": { enabled: true, token: "secret" },
|
||||
},
|
||||
};
|
||||
|
||||
@@ -594,7 +594,7 @@ describe("setupChannels workspace shadow exclusion", () => {
|
||||
|
||||
expect(loadChannelSetupPluginRegistrySnapshotForChannel).not.toHaveBeenCalled();
|
||||
expect(note).toHaveBeenCalledWith(
|
||||
"telegram cannot be configured while plugins disabled. Enable it before setup.",
|
||||
"external-chat cannot be configured while plugins disabled. Enable it before setup.",
|
||||
"Channel setup",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -77,14 +77,20 @@ describe("deriveSessionChatTypeFromKey", () => {
|
||||
{ key: "agent:main:discord:channel:c1", expected: "channel" },
|
||||
{ key: "agent:main:telegram:dm:123456", expected: "direct" },
|
||||
{ key: "telegram:dm:123456", expected: "direct" },
|
||||
{ key: "discord:acc-1:guild-123:channel-456", expected: "channel" },
|
||||
{ key: "12345-678@g.us", expected: "group" },
|
||||
{ key: "agent:main:main", expected: "unknown" },
|
||||
{ key: "agent:main", expected: "unknown" },
|
||||
{ key: "", expected: "unknown" },
|
||||
] as const)("derives chat type for %j => $expected", ({ key, expected }) => {
|
||||
expect(deriveSessionChatTypeFromKey(key)).toBe(expected);
|
||||
});
|
||||
|
||||
it("uses plugin-owned legacy chat-type hooks after generic token parsing", () => {
|
||||
expect(
|
||||
deriveSessionChatTypeFromKey("legacy-room:abc", [
|
||||
(sessionKey) => (sessionKey.startsWith("legacy-room:") ? "channel" : undefined),
|
||||
]),
|
||||
).toBe("channel");
|
||||
});
|
||||
});
|
||||
|
||||
describe("thread session suffix parsing", () => {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
normalizeOptionalLowercaseString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { deriveSessionChatType } from "./session-chat-type.js";
|
||||
|
||||
export type SessionSendPolicyDecision = "allow" | "deny";
|
||||
|
||||
@@ -63,17 +64,9 @@ function deriveChatTypeFromKey(key?: string): SessionChatType | undefined {
|
||||
if (tokens.has("direct") || tokens.has("dm")) {
|
||||
return "direct";
|
||||
}
|
||||
if (/^group:[^:]+$/u.test(normalizedKey)) {
|
||||
return "group";
|
||||
}
|
||||
if (/^[0-9]+(?:-[0-9]+)*@g\.us$/u.test(normalizedKey)) {
|
||||
return "group";
|
||||
}
|
||||
if (/^whatsapp:(?!.*:group:).+@g\.us$/u.test(normalizedKey)) {
|
||||
return "group";
|
||||
}
|
||||
if (/^discord:(?:[^:]+:)?guild-[^:]+:channel-[^:]+$/u.test(normalizedKey)) {
|
||||
return "channel";
|
||||
const derived = deriveSessionChatType(normalizedKey);
|
||||
if (derived !== "unknown") {
|
||||
return derived;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -9,15 +9,6 @@ function deriveBuiltInLegacySessionChatType(
|
||||
if (/^group:[^:]+$/.test(scopedSessionKey)) {
|
||||
return "group";
|
||||
}
|
||||
if (/^[0-9]+(?:-[0-9]+)*@g\.us$/.test(scopedSessionKey)) {
|
||||
return "group";
|
||||
}
|
||||
if (/^whatsapp:(?!.*:group:).+@g\.us$/.test(scopedSessionKey)) {
|
||||
return "group";
|
||||
}
|
||||
if (/^discord:(?:[^:]+:)?guild-[^:]+:channel-[^:]+$/.test(scopedSessionKey)) {
|
||||
return "channel";
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,31 @@ describe("delivery context helpers", () => {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
pluginId: "thread-child-chat",
|
||||
source: "test",
|
||||
plugin: {
|
||||
...createChannelTestPluginBase({
|
||||
id: "thread-child-chat",
|
||||
label: "Thread child chat",
|
||||
}),
|
||||
messaging: {
|
||||
resolveDeliveryTarget: ({
|
||||
conversationId,
|
||||
parentConversationId,
|
||||
}: {
|
||||
conversationId: string;
|
||||
parentConversationId?: string;
|
||||
}) => {
|
||||
const parent = parentConversationId?.trim();
|
||||
const child = conversationId.trim();
|
||||
return parent && parent !== child
|
||||
? { to: `channel:${parent}`, threadId: child }
|
||||
: { to: `channel:${child}` };
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]),
|
||||
);
|
||||
});
|
||||
@@ -149,37 +174,15 @@ describe("delivery context helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
channel: "slack",
|
||||
conversationId: "1710000000.000100",
|
||||
parentConversationId: "C123",
|
||||
expected: { to: "channel:C123", threadId: "1710000000.000100" },
|
||||
},
|
||||
{
|
||||
channel: "telegram",
|
||||
conversationId: "42",
|
||||
parentConversationId: "-10099",
|
||||
expected: { to: "channel:-10099", threadId: "42" },
|
||||
},
|
||||
{
|
||||
channel: "mattermost",
|
||||
conversationId: "msg-child-id",
|
||||
parentConversationId: "channel-parent-id",
|
||||
expected: { to: "channel:channel-parent-id", threadId: "msg-child-id" },
|
||||
},
|
||||
])(
|
||||
"resolves parent-scoped thread delivery targets for $channel",
|
||||
({ channel, conversationId, parentConversationId, expected }) => {
|
||||
expect(
|
||||
resolveConversationDeliveryTarget({
|
||||
channel,
|
||||
conversationId,
|
||||
parentConversationId,
|
||||
}),
|
||||
).toEqual(expected);
|
||||
},
|
||||
);
|
||||
it("resolves parent-scoped thread delivery targets through channel messaging hooks", () => {
|
||||
expect(
|
||||
resolveConversationDeliveryTarget({
|
||||
channel: "thread-child-chat",
|
||||
conversationId: "msg-child-id",
|
||||
parentConversationId: "channel-parent-id",
|
||||
}),
|
||||
).toEqual({ to: "channel:channel-parent-id", threadId: "msg-child-id" });
|
||||
});
|
||||
|
||||
it("derives delivery context from a session entry", () => {
|
||||
expect(
|
||||
|
||||
@@ -67,22 +67,6 @@ export function resolveConversationDeliveryTarget(params: {
|
||||
: typeof params.parentConversationId === "string"
|
||||
? normalizeOptionalString(params.parentConversationId)
|
||||
: undefined;
|
||||
const isThreadChild =
|
||||
conversationId && parentConversationId && parentConversationId !== conversationId;
|
||||
if (channel && isThreadChild) {
|
||||
if (channel === "matrix") {
|
||||
return {
|
||||
to: `room:${parentConversationId}`,
|
||||
threadId: conversationId,
|
||||
};
|
||||
}
|
||||
if (channel === "slack" || channel === "mattermost" || channel === "telegram") {
|
||||
return {
|
||||
to: `channel:${parentConversationId}`,
|
||||
threadId: conversationId,
|
||||
};
|
||||
}
|
||||
}
|
||||
const pluginTarget =
|
||||
channel && conversationId
|
||||
? getChannelPlugin(
|
||||
|
||||
10
tsconfig.core.test.agents.json
Normal file
10
tsconfig.core.test.agents.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.test.json",
|
||||
"include": [
|
||||
"src/**/*.d.ts",
|
||||
"src/agents/**/*.test.ts",
|
||||
"src/agents/**/*.test.tsx",
|
||||
"ui/**/*.d.ts",
|
||||
"packages/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
10
tsconfig.core.test.non-agents.json
Normal file
10
tsconfig.core.test.non-agents.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.core.test.json",
|
||||
"exclude": [
|
||||
"node_modules",
|
||||
"dist",
|
||||
"**/dist/**",
|
||||
"src/agents/**/*.test.ts",
|
||||
"src/agents/**/*.test.tsx"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user