mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
test: clear outbound send service broad matchers
This commit is contained in:
@@ -99,6 +99,46 @@ type ExecuteSendContext = ExecuteSendInput["ctx"];
|
||||
let executePollAction: OutboundSendServiceModule["executePollAction"];
|
||||
let executeSendAction: OutboundSendServiceModule["executeSendAction"];
|
||||
|
||||
type MockCalls = {
|
||||
mock: { calls: unknown[][] };
|
||||
};
|
||||
|
||||
function requireRecord(value: unknown, label: string): Record<string, unknown> {
|
||||
expect(typeof value, label).toBe("object");
|
||||
expect(value, label).not.toBeNull();
|
||||
return value as Record<string, unknown>;
|
||||
}
|
||||
|
||||
function requireArray(value: unknown, label: string): unknown[] {
|
||||
expect(Array.isArray(value), label).toBe(true);
|
||||
return value as unknown[];
|
||||
}
|
||||
|
||||
function expectFields(record: Record<string, unknown>, expected: Record<string, unknown>) {
|
||||
for (const [key, value] of Object.entries(expected)) {
|
||||
expect(record[key], key).toEqual(value);
|
||||
}
|
||||
}
|
||||
|
||||
function expectSingleCallFirstArg(
|
||||
mock: MockCalls,
|
||||
label = "mock first argument",
|
||||
): Record<string, unknown> {
|
||||
expect(mock.mock.calls).toHaveLength(1);
|
||||
const [firstArg] = mock.mock.calls[0] ?? [];
|
||||
return requireRecord(firstArg, label);
|
||||
}
|
||||
|
||||
function expectSingleCallFields(
|
||||
mock: MockCalls,
|
||||
expected: Record<string, unknown>,
|
||||
label?: string,
|
||||
): Record<string, unknown> {
|
||||
const firstArg = expectSingleCallFirstArg(mock, label);
|
||||
expectFields(firstArg, expected);
|
||||
return firstArg;
|
||||
}
|
||||
|
||||
describe("executeSendAction", () => {
|
||||
function pluginActionResult(messageId: string) {
|
||||
return {
|
||||
@@ -121,9 +161,10 @@ describe("executeSendAction", () => {
|
||||
mediaUrls: string[];
|
||||
}>,
|
||||
) {
|
||||
expect(mocks.appendAssistantMessageToSessionTranscript).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...expected, config: {} }),
|
||||
);
|
||||
expectSingleCallFields(mocks.appendAssistantMessageToSessionTranscript, {
|
||||
...expected,
|
||||
config: {},
|
||||
});
|
||||
}
|
||||
|
||||
async function executePluginMirroredSend(params: {
|
||||
@@ -213,14 +254,12 @@ describe("executeSendAction", () => {
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
agentId: "work",
|
||||
channel: "demo-outbound",
|
||||
to: "channel:123",
|
||||
content: "hello",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.sendMessage, {
|
||||
agentId: "work",
|
||||
channel: "demo-outbound",
|
||||
to: "channel:123",
|
||||
content: "hello",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards requesterSenderId to sendMessage on core outbound path", async () => {
|
||||
@@ -245,11 +284,9 @@ describe("executeSendAction", () => {
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSenderId: "attacker",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.sendMessage, {
|
||||
requesterSenderId: "attacker",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards non-id requester sender fields to sendMessage on core outbound path", async () => {
|
||||
@@ -276,13 +313,11 @@ describe("executeSendAction", () => {
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSenderName: "Alice",
|
||||
requesterSenderUsername: "alice_u",
|
||||
requesterSenderE164: "+15551234567",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.sendMessage, {
|
||||
requesterSenderName: "Alice",
|
||||
requesterSenderUsername: "alice_u",
|
||||
requesterSenderE164: "+15551234567",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards requester session context to sendMessage on core outbound path", async () => {
|
||||
@@ -309,14 +344,12 @@ describe("executeSendAction", () => {
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSessionKey: "agent:main:directchat:group:ops",
|
||||
requesterAccountId: "source-account",
|
||||
requesterSenderId: "attacker",
|
||||
accountId: "destination-account",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.sendMessage, {
|
||||
requesterSessionKey: "agent:main:directchat:group:ops",
|
||||
requesterAccountId: "source-account",
|
||||
requesterSenderId: "attacker",
|
||||
accountId: "destination-account",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards requesterSenderId into outbound media access resolution", async () => {
|
||||
@@ -324,11 +357,9 @@ describe("executeSendAction", () => {
|
||||
requesterSenderId: "attacker",
|
||||
});
|
||||
|
||||
expect(mocks.resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSenderId: "attacker",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.resolveAgentScopedOutboundMediaAccess, {
|
||||
requesterSenderId: "attacker",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards non-id requester sender fields into outbound media access resolution", async () => {
|
||||
@@ -338,13 +369,11 @@ describe("executeSendAction", () => {
|
||||
requesterSenderE164: "+15551234567",
|
||||
});
|
||||
|
||||
expect(mocks.resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSenderName: "Alice",
|
||||
requesterSenderUsername: "alice_u",
|
||||
requesterSenderE164: "+15551234567",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.resolveAgentScopedOutboundMediaAccess, {
|
||||
requesterSenderName: "Alice",
|
||||
requesterSenderUsername: "alice_u",
|
||||
requesterSenderE164: "+15551234567",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps requester session channel authoritative for media policy", async () => {
|
||||
@@ -352,12 +381,10 @@ describe("executeSendAction", () => {
|
||||
requesterSenderId: "attacker",
|
||||
});
|
||||
|
||||
expect(mocks.resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:directchat:group:ops",
|
||||
messageProvider: undefined,
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.resolveAgentScopedOutboundMediaAccess, {
|
||||
sessionKey: "agent:main:directchat:group:ops",
|
||||
messageProvider: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("uses requester account for media policy when session context is present", async () => {
|
||||
@@ -367,12 +394,10 @@ describe("executeSendAction", () => {
|
||||
accountId: "destination-account",
|
||||
});
|
||||
|
||||
expect(mocks.resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:directchat:group:ops",
|
||||
accountId: "source-account",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.resolveAgentScopedOutboundMediaAccess, {
|
||||
sessionKey: "agent:main:directchat:group:ops",
|
||||
accountId: "source-account",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to destination account for media policy when requester account is missing", async () => {
|
||||
@@ -381,12 +406,10 @@ describe("executeSendAction", () => {
|
||||
accountId: "destination-account",
|
||||
});
|
||||
|
||||
expect(mocks.resolveAgentScopedOutboundMediaAccess).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:directchat:group:ops",
|
||||
accountId: "destination-account",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.resolveAgentScopedOutboundMediaAccess, {
|
||||
sessionKey: "agent:main:directchat:group:ops",
|
||||
accountId: "destination-account",
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to destination account when forwarding requester context to sendMessage", async () => {
|
||||
@@ -412,12 +435,10 @@ describe("executeSendAction", () => {
|
||||
message: "hello",
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requesterSessionKey: "agent:main:directchat:group:ops",
|
||||
requesterAccountId: "destination-account",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.sendMessage, {
|
||||
requesterSessionKey: "agent:main:directchat:group:ops",
|
||||
requesterAccountId: "destination-account",
|
||||
});
|
||||
});
|
||||
|
||||
it("uses plugin poll action when available", async () => {
|
||||
@@ -488,12 +509,10 @@ describe("executeSendAction", () => {
|
||||
agentId: "agent-1",
|
||||
mediaSources: [],
|
||||
});
|
||||
expect(mocks.dispatchChannelMessageAction).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
mediaLocalRoots: ["/tmp/agent-roots"],
|
||||
mediaReadFile: mocks.createAgentScopedHostMediaReadFile.mock.results[0]?.value,
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.dispatchChannelMessageAction, {
|
||||
mediaLocalRoots: ["/tmp/agent-roots"],
|
||||
mediaReadFile: mocks.createAgentScopedHostMediaReadFile.mock.results[0]?.value,
|
||||
});
|
||||
});
|
||||
|
||||
it("passes concrete media sources when widening plugin dispatch roots", async () => {
|
||||
@@ -581,19 +600,17 @@ describe("executeSendAction", () => {
|
||||
});
|
||||
|
||||
expect(mocks.dispatchChannelMessageAction).not.toHaveBeenCalled();
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "channel:123",
|
||||
content: "hello",
|
||||
dryRun: true,
|
||||
silent: true,
|
||||
gateway: expect.objectContaining({
|
||||
url: "http://127.0.0.1:18789",
|
||||
token: "tok",
|
||||
timeoutMs: 5000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const sendArgs = expectSingleCallFields(mocks.sendMessage, {
|
||||
to: "channel:123",
|
||||
content: "hello",
|
||||
dryRun: true,
|
||||
silent: true,
|
||||
});
|
||||
expectFields(requireRecord(sendArgs.gateway, "send gateway"), {
|
||||
url: "http://127.0.0.1:18789",
|
||||
token: "tok",
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
});
|
||||
|
||||
it("routes prepared plugin send payloads through core best-effort delivery by default", async () => {
|
||||
@@ -631,13 +648,14 @@ describe("executeSendAction", () => {
|
||||
|
||||
expect(prepareSendPayload).toHaveBeenCalled();
|
||||
expect(mocks.dispatchChannelMessageAction).not.toHaveBeenCalled();
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "discord",
|
||||
queuePolicy: "best_effort",
|
||||
payloads: [expect.objectContaining({ channelData: { prepared: true } })],
|
||||
}),
|
||||
);
|
||||
const sendArgs = expectSingleCallFields(mocks.sendMessage, {
|
||||
channel: "discord",
|
||||
queuePolicy: "best_effort",
|
||||
});
|
||||
const [payload] = requireArray(sendArgs.payloads, "send payloads");
|
||||
expectFields(requireRecord(payload, "prepared payload"), {
|
||||
channelData: { prepared: true },
|
||||
});
|
||||
});
|
||||
|
||||
it("uses required core delivery only when the send action opts out of best-effort", async () => {
|
||||
@@ -674,12 +692,10 @@ describe("executeSendAction", () => {
|
||||
bestEffort: false,
|
||||
});
|
||||
|
||||
expect(mocks.sendMessage).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "discord",
|
||||
queuePolicy: "required",
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.sendMessage, {
|
||||
channel: "discord",
|
||||
queuePolicy: "required",
|
||||
});
|
||||
});
|
||||
|
||||
it("forwards poll args to sendPoll on core outbound path", async () => {
|
||||
@@ -714,19 +730,17 @@ describe("executeSendAction", () => {
|
||||
}),
|
||||
});
|
||||
|
||||
expect(mocks.sendPoll).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
channel: "demo-outbound",
|
||||
accountId: "acc-1",
|
||||
to: "channel:123",
|
||||
question: "Lunch?",
|
||||
options: ["Pizza", "Sushi"],
|
||||
maxSelections: 1,
|
||||
durationSeconds: 300,
|
||||
threadId: "thread-1",
|
||||
isAnonymous: true,
|
||||
}),
|
||||
);
|
||||
expectSingleCallFields(mocks.sendPoll, {
|
||||
channel: "demo-outbound",
|
||||
accountId: "acc-1",
|
||||
to: "channel:123",
|
||||
question: "Lunch?",
|
||||
options: ["Pizza", "Sushi"],
|
||||
maxSelections: 1,
|
||||
durationSeconds: 300,
|
||||
threadId: "thread-1",
|
||||
isAnonymous: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips plugin dispatch during dry-run polls and forwards durationHours + silent", async () => {
|
||||
@@ -766,19 +780,17 @@ describe("executeSendAction", () => {
|
||||
});
|
||||
|
||||
expect(mocks.dispatchChannelMessageAction).not.toHaveBeenCalled();
|
||||
expect(mocks.sendPoll).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
to: "channel:123",
|
||||
question: "Lunch?",
|
||||
durationHours: 6,
|
||||
dryRun: true,
|
||||
silent: true,
|
||||
gateway: expect.objectContaining({
|
||||
url: "http://127.0.0.1:18789",
|
||||
token: "tok",
|
||||
timeoutMs: 5000,
|
||||
}),
|
||||
}),
|
||||
);
|
||||
const pollArgs = expectSingleCallFields(mocks.sendPoll, {
|
||||
to: "channel:123",
|
||||
question: "Lunch?",
|
||||
durationHours: 6,
|
||||
dryRun: true,
|
||||
silent: true,
|
||||
});
|
||||
expectFields(requireRecord(pollArgs.gateway, "poll gateway"), {
|
||||
url: "http://127.0.0.1:18789",
|
||||
token: "tok",
|
||||
timeoutMs: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user