mirror of https://github.com/openclaw/openclaw.git
refactor: share auto reply helper fixtures
This commit is contained in:
parent
fd5243c27e
commit
0201f3ff7b
|
|
@ -9,6 +9,20 @@ const baseParams = {
|
|||
replyToMode: "off" as const,
|
||||
};
|
||||
|
||||
async function expectSameTargetRepliesSuppressed(params: { provider: string; to: string }) {
|
||||
const { replyPayloads } = await buildReplyPayloads({
|
||||
...baseParams,
|
||||
payloads: [{ text: "hello world!" }],
|
||||
messageProvider: "heartbeat",
|
||||
originatingChannel: "feishu",
|
||||
originatingTo: "ou_abc123",
|
||||
messagingToolSentTexts: ["different message"],
|
||||
messagingToolSentTargets: [{ tool: "message", provider: params.provider, to: params.to }],
|
||||
});
|
||||
|
||||
expect(replyPayloads).toHaveLength(0);
|
||||
}
|
||||
|
||||
describe("buildReplyPayloads media filter integration", () => {
|
||||
it("strips media URL from payload when in messagingToolSentMediaUrls", async () => {
|
||||
const { replyPayloads } = await buildReplyPayloads({
|
||||
|
|
@ -142,31 +156,11 @@ describe("buildReplyPayloads media filter integration", () => {
|
|||
});
|
||||
|
||||
it("suppresses same-target replies when message tool target provider is generic", async () => {
|
||||
const { replyPayloads } = await buildReplyPayloads({
|
||||
...baseParams,
|
||||
payloads: [{ text: "hello world!" }],
|
||||
messageProvider: "heartbeat",
|
||||
originatingChannel: "feishu",
|
||||
originatingTo: "ou_abc123",
|
||||
messagingToolSentTexts: ["different message"],
|
||||
messagingToolSentTargets: [{ tool: "message", provider: "message", to: "ou_abc123" }],
|
||||
});
|
||||
|
||||
expect(replyPayloads).toHaveLength(0);
|
||||
await expectSameTargetRepliesSuppressed({ provider: "message", to: "ou_abc123" });
|
||||
});
|
||||
|
||||
it("suppresses same-target replies when target provider is channel alias", async () => {
|
||||
const { replyPayloads } = await buildReplyPayloads({
|
||||
...baseParams,
|
||||
payloads: [{ text: "hello world!" }],
|
||||
messageProvider: "heartbeat",
|
||||
originatingChannel: "feishu",
|
||||
originatingTo: "ou_abc123",
|
||||
messagingToolSentTexts: ["different message"],
|
||||
messagingToolSentTargets: [{ tool: "message", provider: "lark", to: "ou_abc123" }],
|
||||
});
|
||||
|
||||
expect(replyPayloads).toHaveLength(0);
|
||||
await expectSameTargetRepliesSuppressed({ provider: "lark", to: "ou_abc123" });
|
||||
});
|
||||
|
||||
it("drops all final payloads when block pipeline streamed successfully", async () => {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import path from "node:path";
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import type { FollowupRun, QueueSettings } from "./queue.js";
|
||||
import { createMockTypingController } from "./test-helpers.js";
|
||||
import { createMockFollowupRun, createMockTypingController } from "./test-helpers.js";
|
||||
|
||||
const runEmbeddedPiAgentMock = vi.fn();
|
||||
const runWithModelFallbackMock = vi.fn();
|
||||
|
|
@ -72,32 +72,15 @@ describe("runReplyAgent media path normalization", () => {
|
|||
|
||||
const result = await runReplyAgent({
|
||||
commandBody: "generate",
|
||||
followupRun: {
|
||||
followupRun: createMockFollowupRun({
|
||||
prompt: "generate",
|
||||
enqueuedAt: Date.now(),
|
||||
run: {
|
||||
agentId: "main",
|
||||
agentDir: "/tmp/agent",
|
||||
sessionId: "session",
|
||||
sessionKey: "main",
|
||||
messageProvider: "telegram",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
config: {},
|
||||
provider: "anthropic",
|
||||
model: "claude",
|
||||
thinkLevel: "low",
|
||||
verboseLevel: "off",
|
||||
elevatedLevel: "off",
|
||||
bashElevated: {
|
||||
enabled: false,
|
||||
allowed: false,
|
||||
defaultLevel: "off",
|
||||
},
|
||||
timeoutMs: 1_000,
|
||||
blockReplyBreak: "message_end",
|
||||
},
|
||||
} as unknown as FollowupRun,
|
||||
}) as unknown as FollowupRun,
|
||||
queueKey: "main",
|
||||
resolvedQueue: { mode: "interrupt" } as QueueSettings,
|
||||
shouldSteer: false,
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import {
|
|||
} from "../../../acp/conversation-id.js";
|
||||
import { DISCORD_THREAD_BINDING_CHANNEL } from "../../../channels/thread-bindings-policy.js";
|
||||
import { resolveConversationIdFromTargets } from "../../../infra/outbound/conversation-id.js";
|
||||
import { parseAgentSessionKey } from "../../../routing/session-key.js";
|
||||
import type { HandleCommandsParams } from "../commands-types.js";
|
||||
import { parseDiscordParentChannelFromSessionKey } from "../discord-parent-channel.js";
|
||||
import { resolveTelegramConversationId } from "../telegram-context.js";
|
||||
|
||||
export function resolveAcpCommandChannel(params: HandleCommandsParams): string {
|
||||
|
|
@ -64,19 +64,6 @@ export function resolveAcpCommandConversationId(params: HandleCommandsParams): s
|
|||
});
|
||||
}
|
||||
|
||||
function parseDiscordParentChannelFromSessionKey(raw: unknown): string | undefined {
|
||||
const sessionKey = normalizeConversationText(raw);
|
||||
if (!sessionKey) {
|
||||
return undefined;
|
||||
}
|
||||
const scoped = parseAgentSessionKey(sessionKey)?.rest ?? sessionKey.toLowerCase();
|
||||
const match = scoped.match(/(?:^|:)channel:([^:]+)$/);
|
||||
if (!match?.[1]) {
|
||||
return undefined;
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function parseDiscordParentChannelFromContext(raw: unknown): string | undefined {
|
||||
const parentId = normalizeConversationText(raw);
|
||||
if (!parentId) {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,5 @@
|
|||
import { getChannelDock } from "../../channels/dock.js";
|
||||
import {
|
||||
authorizeConfigWrite,
|
||||
canBypassConfigWritePolicy,
|
||||
formatConfigWriteDeniedMessage,
|
||||
resolveExplicitConfigWriteTarget,
|
||||
} from "../../channels/plugins/config-writes.js";
|
||||
import { resolveExplicitConfigWriteTarget } from "../../channels/plugins/config-writes.js";
|
||||
import { listPairingChannels } from "../../channels/plugins/pairing.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import { normalizeChannelId } from "../../channels/registry.js";
|
||||
|
|
@ -36,6 +31,7 @@ import { resolveTelegramAccount } from "../../telegram/accounts.js";
|
|||
import { resolveWhatsAppAccount } from "../../web/accounts.js";
|
||||
import { rejectUnauthorizedCommand, requireCommandFlagEnabled } from "./command-gates.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
import { resolveConfigWriteDeniedText } from "./config-write-authorization.js";
|
||||
|
||||
type AllowlistScope = "dm" | "group" | "all";
|
||||
type AllowlistAction = "list" | "add" | "remove";
|
||||
|
|
@ -628,20 +624,19 @@ export const handleAllowlistCommand: CommandHandler = async (params, allowTextCo
|
|||
accountId: normalizedAccountId,
|
||||
writeTarget,
|
||||
} = resolveAccountTarget(parsedConfig, channelId, accountId);
|
||||
const writeAuth = authorizeConfigWrite({
|
||||
const deniedText = resolveConfigWriteDeniedText({
|
||||
cfg: params.cfg,
|
||||
origin: { channelId, accountId: params.ctx.AccountId },
|
||||
target: writeTarget,
|
||||
allowBypass: canBypassConfigWritePolicy({
|
||||
channel: params.command.channel,
|
||||
channelId,
|
||||
accountId: params.ctx.AccountId,
|
||||
gatewayClientScopes: params.ctx.GatewayClientScopes,
|
||||
}),
|
||||
target: writeTarget,
|
||||
});
|
||||
if (!writeAuth.allowed) {
|
||||
if (deniedText) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: formatConfigWriteDeniedMessage({ result: writeAuth, fallbackChannelId: channelId }),
|
||||
text: deniedText,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,4 @@
|
|||
import {
|
||||
authorizeConfigWrite,
|
||||
canBypassConfigWritePolicy,
|
||||
formatConfigWriteDeniedMessage,
|
||||
resolveConfigWriteTargetFromPath,
|
||||
} from "../../channels/plugins/config-writes.js";
|
||||
import { resolveConfigWriteTargetFromPath } from "../../channels/plugins/config-writes.js";
|
||||
import { normalizeChannelId } from "../../channels/registry.js";
|
||||
import {
|
||||
getConfigValueAtPath,
|
||||
|
|
@ -31,6 +26,7 @@ import {
|
|||
} from "./command-gates.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
import { parseConfigCommand } from "./config-commands.js";
|
||||
import { resolveConfigWriteDeniedText } from "./config-write-authorization.js";
|
||||
import { parseDebugCommand } from "./debug-commands.js";
|
||||
|
||||
export const handleConfigCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
|
|
@ -84,20 +80,19 @@ export const handleConfigCommand: CommandHandler = async (params, allowTextComma
|
|||
}
|
||||
parsedWritePath = parsedPath.path;
|
||||
const channelId = params.command.channelId ?? normalizeChannelId(params.command.channel);
|
||||
const writeAuth = authorizeConfigWrite({
|
||||
const deniedText = resolveConfigWriteDeniedText({
|
||||
cfg: params.cfg,
|
||||
origin: { channelId, accountId: params.ctx.AccountId },
|
||||
target: resolveConfigWriteTargetFromPath(parsedWritePath),
|
||||
allowBypass: canBypassConfigWritePolicy({
|
||||
channel: params.command.channel,
|
||||
channelId,
|
||||
accountId: params.ctx.AccountId,
|
||||
gatewayClientScopes: params.ctx.GatewayClientScopes,
|
||||
}),
|
||||
target: resolveConfigWriteTargetFromPath(parsedWritePath),
|
||||
});
|
||||
if (!writeAuth.allowed) {
|
||||
if (deniedText) {
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text: formatConfigWriteDeniedMessage({ result: writeAuth, fallbackChannelId: channelId }),
|
||||
text: deniedText,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,33 @@
|
|||
import {
|
||||
authorizeConfigWrite,
|
||||
canBypassConfigWritePolicy,
|
||||
formatConfigWriteDeniedMessage,
|
||||
} from "../../channels/plugins/config-writes.js";
|
||||
import type { ChannelId } from "../../channels/plugins/types.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
|
||||
export function resolveConfigWriteDeniedText(params: {
|
||||
cfg: OpenClawConfig;
|
||||
channel?: string | null;
|
||||
channelId: ChannelId | null;
|
||||
accountId?: string;
|
||||
gatewayClientScopes?: string[];
|
||||
target: Parameters<typeof authorizeConfigWrite>[0]["target"];
|
||||
}): string | null {
|
||||
const writeAuth = authorizeConfigWrite({
|
||||
cfg: params.cfg,
|
||||
origin: { channelId: params.channelId, accountId: params.accountId },
|
||||
target: params.target,
|
||||
allowBypass: canBypassConfigWritePolicy({
|
||||
channel: params.channel ?? "",
|
||||
gatewayClientScopes: params.gatewayClientScopes,
|
||||
}),
|
||||
});
|
||||
if (writeAuth.allowed) {
|
||||
return null;
|
||||
}
|
||||
return formatConfigWriteDeniedMessage({
|
||||
result: writeAuth,
|
||||
fallbackChannelId: params.channelId,
|
||||
});
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { normalizeConversationText } from "../../acp/conversation-id.js";
|
||||
import { parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
|
||||
export function parseDiscordParentChannelFromSessionKey(raw: unknown): string | undefined {
|
||||
const sessionKey = normalizeConversationText(raw);
|
||||
if (!sessionKey) {
|
||||
return undefined;
|
||||
}
|
||||
const scoped = parseAgentSessionKey(sessionKey)?.rest ?? sessionKey.toLowerCase();
|
||||
const match = scoped.match(/(?:^|:)channel:([^:]+)$/);
|
||||
if (!match?.[1]) {
|
||||
return undefined;
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
|
@ -4,7 +4,7 @@ import path from "node:path";
|
|||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { loadSessionStore, saveSessionStore, type SessionEntry } from "../../config/sessions.js";
|
||||
import type { FollowupRun } from "./queue.js";
|
||||
import { createMockTypingController } from "./test-helpers.js";
|
||||
import { createMockFollowupRun, createMockTypingController } from "./test-helpers.js";
|
||||
|
||||
const runEmbeddedPiAgentMock = vi.fn();
|
||||
const routeReplyMock = vi.fn();
|
||||
|
|
@ -50,47 +50,12 @@ beforeEach(() => {
|
|||
});
|
||||
|
||||
const baseQueuedRun = (messageProvider = "whatsapp"): FollowupRun =>
|
||||
({
|
||||
prompt: "hello",
|
||||
summaryLine: "hello",
|
||||
enqueuedAt: Date.now(),
|
||||
originatingTo: "channel:C1",
|
||||
run: {
|
||||
sessionId: "session",
|
||||
sessionKey: "main",
|
||||
messageProvider,
|
||||
agentAccountId: "primary",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
config: {},
|
||||
skillsSnapshot: {},
|
||||
provider: "anthropic",
|
||||
model: "claude",
|
||||
thinkLevel: "low",
|
||||
verboseLevel: "off",
|
||||
elevatedLevel: "off",
|
||||
bashElevated: {
|
||||
enabled: false,
|
||||
allowed: false,
|
||||
defaultLevel: "off",
|
||||
},
|
||||
timeoutMs: 1_000,
|
||||
blockReplyBreak: "message_end",
|
||||
},
|
||||
}) as FollowupRun;
|
||||
createMockFollowupRun({ run: { messageProvider } });
|
||||
|
||||
function createQueuedRun(
|
||||
overrides: Partial<Omit<FollowupRun, "run">> & { run?: Partial<FollowupRun["run"]> } = {},
|
||||
): FollowupRun {
|
||||
const base = baseQueuedRun();
|
||||
return {
|
||||
...base,
|
||||
...overrides,
|
||||
run: {
|
||||
...base.run,
|
||||
...overrides.run,
|
||||
},
|
||||
};
|
||||
return createMockFollowupRun(overrides);
|
||||
}
|
||||
|
||||
function mockCompactionRun(params: {
|
||||
|
|
|
|||
|
|
@ -34,11 +34,12 @@ import { resolveConversationIdFromTargets } from "../../infra/outbound/conversat
|
|||
import { deliverSessionMaintenanceWarning } from "../../infra/session-maintenance-warning.js";
|
||||
import { createSubsystemLogger } from "../../logging/subsystem.js";
|
||||
import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
||||
import { normalizeMainKey, parseAgentSessionKey } from "../../routing/session-key.js";
|
||||
import { normalizeMainKey } from "../../routing/session-key.js";
|
||||
import { normalizeSessionDeliveryFields } from "../../utils/delivery-context.js";
|
||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import type { MsgContext, TemplateContext } from "../templating.js";
|
||||
import { resolveEffectiveResetTargetSessionKey } from "./acp-reset-target.js";
|
||||
import { parseDiscordParentChannelFromSessionKey } from "./discord-parent-channel.js";
|
||||
import { normalizeInboundTextNewlines } from "./inbound-text.js";
|
||||
import { stripMentions, stripStructuralPrefixes } from "./mentions.js";
|
||||
import {
|
||||
|
|
@ -70,19 +71,6 @@ export type SessionInitResult = {
|
|||
triggerBodyNormalized: string;
|
||||
};
|
||||
|
||||
function parseDiscordParentChannelFromSessionKey(raw: unknown): string | undefined {
|
||||
const sessionKey = normalizeConversationText(raw);
|
||||
if (!sessionKey) {
|
||||
return undefined;
|
||||
}
|
||||
const scoped = parseAgentSessionKey(sessionKey)?.rest ?? sessionKey.toLowerCase();
|
||||
const match = scoped.match(/(?:^|:)channel:([^:]+)$/);
|
||||
if (!match?.[1]) {
|
||||
return undefined;
|
||||
}
|
||||
return match[1];
|
||||
}
|
||||
|
||||
function resolveAcpResetBindingContext(ctx: MsgContext): {
|
||||
channel: string;
|
||||
accountId: string;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { vi } from "vitest";
|
||||
import type { FollowupRun } from "./queue.js";
|
||||
import type { TypingController } from "./typing.js";
|
||||
|
||||
export function createMockTypingController(
|
||||
|
|
@ -16,3 +17,44 @@ export function createMockTypingController(
|
|||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function createMockFollowupRun(
|
||||
overrides: Partial<Omit<FollowupRun, "run">> & { run?: Partial<FollowupRun["run"]> } = {},
|
||||
): FollowupRun {
|
||||
const base: FollowupRun = {
|
||||
prompt: "hello",
|
||||
summaryLine: "hello",
|
||||
enqueuedAt: Date.now(),
|
||||
originatingTo: "channel:C1",
|
||||
run: {
|
||||
sessionId: "session",
|
||||
sessionKey: "main",
|
||||
messageProvider: "whatsapp",
|
||||
agentAccountId: "primary",
|
||||
sessionFile: "/tmp/session.jsonl",
|
||||
workspaceDir: "/tmp",
|
||||
config: {},
|
||||
skillsSnapshot: {},
|
||||
provider: "anthropic",
|
||||
model: "claude",
|
||||
thinkLevel: "low",
|
||||
verboseLevel: "off",
|
||||
elevatedLevel: "off",
|
||||
bashElevated: {
|
||||
enabled: false,
|
||||
allowed: false,
|
||||
defaultLevel: "off",
|
||||
},
|
||||
timeoutMs: 1_000,
|
||||
blockReplyBreak: "message_end",
|
||||
},
|
||||
};
|
||||
return {
|
||||
...base,
|
||||
...overrides,
|
||||
run: {
|
||||
...base.run,
|
||||
...overrides.run,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue