fix(mattermost): make replay retries explicit

This commit is contained in:
Vincent Koc
2026-04-13 15:42:09 +01:00
parent 2677f7cf14
commit fad06f7c21
2 changed files with 643 additions and 483 deletions

View File

@@ -1,8 +1,11 @@
import { createClaimableDedupe } from "openclaw/plugin-sdk/persistent-dedupe";
import { describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../../runtime-api.js";
import { resolveMattermostAccount } from "./accounts.js";
import {
evaluateMattermostMentionGate,
MattermostRetryableInboundError,
processMattermostReplayGuardedPost,
resolveMattermostReactionChannelId,
resolveMattermostEffectiveReplyToId,
resolveMattermostReplyRootId,
@@ -281,6 +284,100 @@ describe("resolveMattermostThreadSessionContext", () => {
});
});
describe("processMattermostReplayGuardedPost", () => {
it("skips duplicate message batches after a successful commit", async () => {
const replayGuard = createClaimableDedupe({
ttlMs: 10_000,
memoryMaxSize: 100,
});
const handlePost = vi.fn(async () => undefined);
await expect(
processMattermostReplayGuardedPost({
replayGuard,
accountId: "acct",
messageIds: ["post-1"],
handlePost,
}),
).resolves.toBe("processed");
await expect(
processMattermostReplayGuardedPost({
replayGuard,
accountId: "acct",
messageIds: ["post-1"],
handlePost,
}),
).resolves.toBe("duplicate");
expect(handlePost).toHaveBeenCalledTimes(1);
});
it("releases claims for explicit retryable failures", async () => {
const replayGuard = createClaimableDedupe({
ttlMs: 10_000,
memoryMaxSize: 100,
});
let attempts = 0;
const handlePost = vi.fn(async () => {
attempts += 1;
if (attempts === 1) {
throw new MattermostRetryableInboundError("retry me");
}
});
await expect(
processMattermostReplayGuardedPost({
replayGuard,
accountId: "acct",
messageIds: ["post-2"],
handlePost,
}),
).rejects.toThrow("retry me");
await expect(
processMattermostReplayGuardedPost({
replayGuard,
accountId: "acct",
messageIds: ["post-2"],
handlePost,
}),
).resolves.toBe("processed");
expect(handlePost).toHaveBeenCalledTimes(2);
});
it("keeps replay committed after a non-retryable failure", async () => {
const replayGuard = createClaimableDedupe({
ttlMs: 10_000,
memoryMaxSize: 100,
});
const visibleSideEffect = vi.fn();
const handlePost = vi.fn(async () => {
visibleSideEffect();
throw new Error("post-send failure");
});
await expect(
processMattermostReplayGuardedPost({
replayGuard,
accountId: "acct",
messageIds: ["post-3"],
handlePost,
}),
).rejects.toThrow("post-send failure");
await expect(
processMattermostReplayGuardedPost({
replayGuard,
accountId: "acct",
messageIds: ["post-3"],
handlePost,
}),
).resolves.toBe("duplicate");
expect(handlePost).toHaveBeenCalledTimes(1);
expect(visibleSideEffect).toHaveBeenCalledTimes(1);
});
});
describe("resolveMattermostReactionChannelId", () => {
it("prefers broadcast channel_id when present", () => {
expect(

File diff suppressed because it is too large Load Diff