fix(mattermost): normalize send action reply fallback

This commit is contained in:
Muhammed Mukhthar CM
2026-03-10 06:40:15 +00:00
parent 2f5d68d5f5
commit 33cac4c33f
3 changed files with 67 additions and 9 deletions

View File

@@ -54,6 +54,7 @@ Docs: https://docs.openclaw.ai
- Agents/memory flush: forward `memoryFlushWritePath` through `runEmbeddedPiAgent` so memory-triggered flush turns keep the append-only write guard without aborting before tool setup. Follows up on #38574. (#41761) Thanks @frankekn.
- CI/CodeQL Swift toolchain: select Xcode 26.1 before installing Swift build tools so the CodeQL Swift job uses Swift tools 6.2 on `macos-latest`. (#41787) thanks @BunsDev.
- Sandbox/subagents: pass the real configured workspace through `sessions_spawn` inheritance when a parent agent runs in a copied-workspace sandbox, so child `/agent` mounts point at the configured workspace instead of the parent sandbox copy. (#40757) Thanks @dsantoreis.
- Mattermost/plugin send actions: normalize direct `replyTo` fallback handling so threaded plugin sends trim blank IDs and reuse the correct reply target again. (#41176) Thanks @hnykda.
## 2026.3.8

View File

@@ -214,6 +214,57 @@ describe("mattermostPlugin", () => {
]);
expect(result?.details).toEqual({});
});
it("maps replyTo to replyToId for send actions", async () => {
const cfg = createMattermostTestConfig();
await mattermostPlugin.actions?.handleAction?.({
channel: "mattermost",
action: "send",
params: {
to: "channel:CHAN1",
message: "hello",
replyTo: "post-root",
},
cfg,
accountId: "default",
} as any);
expect(sendMessageMattermostMock).toHaveBeenCalledWith(
"channel:CHAN1",
"hello",
expect.objectContaining({
accountId: "default",
replyToId: "post-root",
}),
);
});
it("falls back to trimmed replyTo when replyToId is blank", async () => {
const cfg = createMattermostTestConfig();
await mattermostPlugin.actions?.handleAction?.({
channel: "mattermost",
action: "send",
params: {
to: "channel:CHAN1",
message: "hello",
replyToId: " ",
replyTo: " post-root ",
},
cfg,
accountId: "default",
} as any);
expect(sendMessageMattermostMock).toHaveBeenCalledWith(
"channel:CHAN1",
"hello",
expect.objectContaining({
accountId: "default",
replyToId: "post-root",
}),
);
});
});
describe("outbound", () => {

View File

@@ -157,15 +157,9 @@ const mattermostMessageActions: ChannelMessageActionAdapter = {
}
const message = typeof params.message === "string" ? params.message : "";
// The message tool passes the reply target as "replyTo", not "replyToId".
// handleSendAction reads params.replyTo into a local var but never writes
// it back, so the plugin handler must check both property names.
const replyToId =
typeof params.replyToId === "string"
? params.replyToId
: typeof params.replyTo === "string"
? params.replyTo
: undefined;
// Match the shared runner semantics: trim empty reply IDs away before
// falling back from replyToId to replyTo on direct plugin calls.
const replyToId = readMattermostReplyToId(params);
const resolvedAccountId = accountId || undefined;
const mediaUrl =
@@ -209,6 +203,18 @@ const meta = {
quickstartAllowFrom: true,
} as const;
function readMattermostReplyToId(params: Record<string, unknown>): string | undefined {
const readNormalizedValue = (value: unknown) => {
if (typeof value !== "string") {
return undefined;
}
const trimmed = value.trim();
return trimmed || undefined;
};
return readNormalizedValue(params.replyToId) ?? readNormalizedValue(params.replyTo);
}
function normalizeAllowEntry(entry: string): string {
return entry
.trim()