Files
openclaw/extensions/msteams/src/errors.test.ts
root e0b91067e3 fix(msteams): add proactive fallback for revoked turn context
Fixes #27189

When an inbound message is debounced, the Bot Framework turn context is
revoked before the debouncer flushes and the reply is dispatched. Any
attempt to use the revoked context proxy throws a TypeError, causing the
reply to fail silently.

This commit fixes the issue by adding a fallback to proactive messaging
when the turn context is revoked:

- `isRevokedProxyError()`: New error utility to reliably detect when a
  proxy has been revoked.

- `reply-dispatcher.ts`: `sendTypingIndicator` now catches revoked proxy
  errors and falls back to sending the typing indicator via
  `adapter.continueConversation`.

- `messenger.ts`: `sendMSTeamsMessages` now catches revoked proxy errors
  when `replyStyle` is `thread` and falls back to proactive messaging.

This ensures that replies are delivered reliably even when the inbound
message was debounced, resolving the core issue where the bot appeared
to ignore messages.
2026-03-02 20:49:03 +00:00

71 lines
2.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
classifyMSTeamsSendError,
formatMSTeamsSendErrorHint,
formatUnknownError,
isRevokedProxyError,
} from "./errors.js";
describe("msteams errors", () => {
it("formats unknown errors", () => {
expect(formatUnknownError("oops")).toBe("oops");
expect(formatUnknownError(null)).toBe("null");
});
it("classifies auth errors", () => {
expect(classifyMSTeamsSendError({ statusCode: 401 }).kind).toBe("auth");
expect(classifyMSTeamsSendError({ statusCode: 403 }).kind).toBe("auth");
});
it("classifies throttling errors and parses retry-after", () => {
expect(classifyMSTeamsSendError({ statusCode: 429, retryAfter: "1.5" })).toMatchObject({
kind: "throttled",
statusCode: 429,
retryAfterMs: 1500,
});
});
it("classifies transient errors", () => {
expect(classifyMSTeamsSendError({ statusCode: 503 })).toMatchObject({
kind: "transient",
statusCode: 503,
});
});
it("classifies permanent 4xx errors", () => {
expect(classifyMSTeamsSendError({ statusCode: 400 })).toMatchObject({
kind: "permanent",
statusCode: 400,
});
});
it("provides actionable hints for common cases", () => {
expect(formatMSTeamsSendErrorHint({ kind: "auth" })).toContain("msteams");
expect(formatMSTeamsSendErrorHint({ kind: "throttled" })).toContain("throttled");
});
describe("isRevokedProxyError", () => {
it("returns true for revoked proxy TypeError", () => {
expect(
isRevokedProxyError(new TypeError("Cannot perform 'set' on a proxy that has been revoked")),
).toBe(true);
expect(
isRevokedProxyError(new TypeError("Cannot perform 'get' on a proxy that has been revoked")),
).toBe(true);
});
it("returns false for non-TypeError errors", () => {
expect(isRevokedProxyError(new Error("proxy that has been revoked"))).toBe(false);
});
it("returns false for unrelated TypeErrors", () => {
expect(isRevokedProxyError(new TypeError("undefined is not a function"))).toBe(false);
});
it("returns false for non-error values", () => {
expect(isRevokedProxyError(null)).toBe(false);
expect(isRevokedProxyError("proxy that has been revoked")).toBe(false);
});
});
});