chore: merge origin/main into main

This commit is contained in:
Peter Steinberger
2026-02-22 13:42:52 +00:00
304 changed files with 17041 additions and 5502 deletions

View File

@@ -0,0 +1,29 @@
import { describe, expect, it } from "vitest";
import { resolveTelegramRuntimeGroupPolicy } from "./group-access.js";
describe("resolveTelegramRuntimeGroupPolicy", () => {
it("fails closed when channels.telegram is missing and no defaults are set", () => {
const resolved = resolveTelegramRuntimeGroupPolicy({
providerConfigPresent: false,
});
expect(resolved.groupPolicy).toBe("allowlist");
expect(resolved.providerMissingFallbackApplied).toBe(true);
});
it("keeps open fallback when channels.telegram is configured", () => {
const resolved = resolveTelegramRuntimeGroupPolicy({
providerConfigPresent: true,
});
expect(resolved.groupPolicy).toBe("open");
expect(resolved.providerMissingFallbackApplied).toBe(false);
});
it("ignores explicit defaults when provider config is missing", () => {
const resolved = resolveTelegramRuntimeGroupPolicy({
providerConfigPresent: false,
defaultGroupPolicy: "disabled",
});
expect(resolved.groupPolicy).toBe("allowlist");
expect(resolved.providerMissingFallbackApplied).toBe(true);
});
});

View File

@@ -1,5 +1,6 @@
import type { OpenClawConfig } from "../config/config.js";
import type { ChannelGroupPolicy } from "../config/group-policy.js";
import { resolveOpenProviderRuntimeGroupPolicy } from "../config/runtime-group-policy.js";
import type {
TelegramAccountConfig,
TelegramGroupConfig,
@@ -72,6 +73,17 @@ export type TelegramGroupPolicyAccessResult =
groupPolicy: "open" | "disabled" | "allowlist";
};
export const resolveTelegramRuntimeGroupPolicy = (params: {
providerConfigPresent: boolean;
groupPolicy?: TelegramAccountConfig["groupPolicy"];
defaultGroupPolicy?: TelegramAccountConfig["groupPolicy"];
}) =>
resolveOpenProviderRuntimeGroupPolicy({
providerConfigPresent: params.providerConfigPresent,
groupPolicy: params.groupPolicy,
defaultGroupPolicy: params.defaultGroupPolicy,
});
export const evaluateTelegramGroupPolicyAccess = (params: {
isGroup: boolean;
chatId: string | number;
@@ -90,20 +102,21 @@ export const evaluateTelegramGroupPolicyAccess = (params: {
requireSenderForAllowlistAuthorization: boolean;
checkChatAllowlist: boolean;
}): TelegramGroupPolicyAccessResult => {
const { groupPolicy: runtimeFallbackPolicy } = resolveTelegramRuntimeGroupPolicy({
providerConfigPresent: params.cfg.channels?.telegram !== undefined,
groupPolicy: params.telegramCfg.groupPolicy,
defaultGroupPolicy: params.cfg.channels?.defaults?.groupPolicy,
});
const fallbackPolicy =
firstDefined(
params.telegramCfg.groupPolicy,
params.cfg.channels?.defaults?.groupPolicy,
"open",
) ?? "open";
firstDefined(params.telegramCfg.groupPolicy, params.cfg.channels?.defaults?.groupPolicy) ??
runtimeFallbackPolicy;
const groupPolicy = params.useTopicAndGroupOverrides
? (firstDefined(
params.topicConfig?.groupPolicy,
params.groupConfig?.groupPolicy,
params.telegramCfg.groupPolicy,
params.cfg.channels?.defaults?.groupPolicy,
"open",
) ?? "open")
) ?? runtimeFallbackPolicy)
: fallbackPolicy;
if (!params.isGroup || !params.enforcePolicy) {

View File

@@ -169,8 +169,12 @@ describe("monitorTelegramProvider (grammY)", () => {
expect(api.sendMessage).not.toHaveBeenCalled();
});
it("retries on recoverable network errors", async () => {
const networkError = Object.assign(new Error("timeout"), { code: "ETIMEDOUT" });
it("retries on recoverable undici fetch errors", async () => {
const networkError = Object.assign(new TypeError("fetch failed"), {
cause: Object.assign(new Error("connect timeout"), {
code: "UND_ERR_CONNECT_TIMEOUT",
}),
});
runSpy
.mockImplementationOnce(() => ({
task: () => Promise.reject(networkError),

View File

@@ -30,12 +30,25 @@ describe("isRecoverableTelegramNetworkError", () => {
expect(isRecoverableTelegramNetworkError(new Error("Undici: socket failure"))).toBe(true);
});
it("skips message matches for send context", () => {
it("treats undici fetch failed errors as recoverable in send context", () => {
const err = new TypeError("fetch failed");
expect(isRecoverableTelegramNetworkError(err, { context: "send" })).toBe(false);
expect(isRecoverableTelegramNetworkError(err, { context: "send" })).toBe(true);
expect(
isRecoverableTelegramNetworkError(new Error("TypeError: fetch failed"), { context: "send" }),
).toBe(true);
expect(isRecoverableTelegramNetworkError(err, { context: "polling" })).toBe(true);
});
it("skips broad message matches for send context", () => {
const networkRequestErr = new Error("Network request for 'sendMessage' failed!");
expect(isRecoverableTelegramNetworkError(networkRequestErr, { context: "send" })).toBe(false);
expect(isRecoverableTelegramNetworkError(networkRequestErr, { context: "polling" })).toBe(true);
const undiciSnippetErr = new Error("Undici: socket failure");
expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "send" })).toBe(false);
expect(isRecoverableTelegramNetworkError(undiciSnippetErr, { context: "polling" })).toBe(true);
});
it("returns false for unrelated errors", () => {
expect(isRecoverableTelegramNetworkError(new Error("invalid token"))).toBe(false);
});

View File

@@ -27,9 +27,9 @@ const RECOVERABLE_ERROR_NAMES = new Set([
"BodyTimeoutError",
]);
const ALWAYS_RECOVERABLE_MESSAGES = new Set(["fetch failed", "typeerror: fetch failed"]);
const RECOVERABLE_MESSAGE_SNIPPETS = [
"fetch failed",
"typeerror: fetch failed",
"undici",
"network error",
"network request",
@@ -138,9 +138,12 @@ export function isRecoverableTelegramNetworkError(
return true;
}
if (allowMessageMatch) {
const message = formatErrorMessage(candidate).toLowerCase();
if (message && RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) {
const message = formatErrorMessage(candidate).trim().toLowerCase();
if (message && ALWAYS_RECOVERABLE_MESSAGES.has(message)) {
return true;
}
if (allowMessageMatch && message) {
if (RECOVERABLE_MESSAGE_SNIPPETS.some((snippet) => message.includes(snippet))) {
return true;
}
}