fix(cron): preserve canonical failure alert routes (#116933)

This commit is contained in:
Peter Steinberger
2026-08-09 07:17:56 -07:00
committed by GitHub
parent 00194139ba
commit 2433fa213c
2 changed files with 74 additions and 16 deletions
@@ -9,25 +9,64 @@ import { resolveFailureAlert } from "./failure-alerts.js";
import { createCronServiceState, type DeferredCronNotifications } from "./state.js";
import { applyJobResult } from "./timer.js";
function stripTestTargetPrefix(raw: string, prefixes: readonly string[]): string | undefined {
const target = raw
.trim()
.replace(new RegExp(`^(?:${prefixes.join("|")}):`, "i"), "")
.trim();
return target || undefined;
}
function normalizeDiscordTestTarget(raw: string): string | undefined {
const target = raw.trim().toLowerCase();
if (!target) {
return undefined;
}
if (target.startsWith("discord:channel:")) {
return target.slice("discord:".length);
}
if (target.startsWith("discord:")) {
return `user:${target.slice("discord:".length)}`;
}
return /^(channel|user):/.test(target) ? target : `channel:${target}`;
}
describe("cron failure alert account routing", () => {
beforeEach(() => {
setActivePluginRegistry(
createTestRegistry(
[
{ id: "telegram", aliases: [], targetPrefixes: ["telegram", "tg"] },
const pluginSpecs = [
{
id: "telegram",
aliases: [],
targetPrefixes: ["telegram", "tg"],
normalizeTarget: (raw: string) => {
const target = stripTestTargetPrefix(raw, ["telegram", "tg"]);
return target ? `telegram:${target}` : undefined;
},
},
{
id: "googlechat",
aliases: ["gchat", "google-chat"],
targetPrefixes: ["googlechat", "google-chat", "gchat"],
normalizeTarget: (raw: string) =>
stripTestTargetPrefix(raw, ["googlechat", "google-chat", "gchat"]),
},
].map(({ id, aliases, targetPrefixes }) => {
{
id: "discord",
aliases: [],
targetPrefixes: ["discord"],
normalizeTarget: normalizeDiscordTestTarget,
},
];
setActivePluginRegistry(
createTestRegistry(
pluginSpecs.map(({ id, aliases, targetPrefixes, normalizeTarget }) => {
const plugin = createChannelTestPluginBase({ id });
return {
pluginId: id,
plugin: {
...plugin,
meta: { ...plugin.meta, aliases },
messaging: { targetPrefixes },
messaging: { targetPrefixes, normalizeTarget },
},
source: `test:${id}`,
};
@@ -199,6 +238,19 @@ describe("cron failure alert account routing", () => {
jobAlert: undefined,
expected: { channel: "slack", to: undefined, accountId: undefined },
},
{
name: "does not equate Discord user and channel targets with the same id",
globalAlert: { enabled: true, after: 1 },
deliveryChannel: "discord",
deliveryTo: "1234567890",
jobAlert: { channel: "discord", to: "discord:1234567890" },
expected: {
channel: "discord",
to: "discord:1234567890",
accountId: undefined,
threadId: undefined,
},
},
{
name: "does not inherit the primary account for a webhook",
globalAlert: {
@@ -309,6 +361,13 @@ describe("cron failure alert account routing", () => {
failureAlert: { channel: "googlechat", to: "googlechat:RoomA" },
expectedChannel: "googlechat",
},
{
name: "plugin-normalized equivalent",
deliveryChannel: "discord",
deliveryTo: "1234567890",
failureAlert: { channel: "discord", to: "discord:channel:1234567890" },
expectedChannel: "discord",
},
])("keeps the primary account and topic on $name failure alerts", (testCase) => {
const { failureAlert } = testCase;
const sendCronFailureAlert = vi.fn(async () => undefined);
+6 -7
View File
@@ -3,10 +3,8 @@ import { normalizeOptionalLowercaseString } from "@openclaw/normalization-core/s
import { truncateUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import type { FailoverReason } from "../../agents/embedded-agent-helpers/types.js";
import { normalizeAnyChannelId } from "../../channels/registry-normalize.js";
import {
resolveTargetPrefixedChannel,
stripTargetProviderPrefix,
} from "../../infra/outbound/channel-target-prefix.js";
import { resolveTargetPrefixedChannel } from "../../infra/outbound/channel-target-prefix.js";
import { normalizeTargetForProvider } from "../../infra/outbound/target-normalization.js";
import type { CronFailureNotificationDelivery, CronJob, CronMessageChannel } from "../types.js";
import type { CronServiceState, DeferredCronNotifications } from "./state.js";
@@ -53,11 +51,12 @@ function resolveFailureAlertChannel(channel: unknown, to?: string): CronMessageC
}
function normalizeFailureAlertRecipient(channel: CronMessageChannel, to: string): string {
if (resolveTargetPrefixedChannel(to) !== channel) {
try {
return normalizeTargetForProvider(channel, to) ?? to;
} catch {
// Invalid loaded targets are distinct routes; they must not block run finalization.
return to;
}
// Canonicalize loaded-provider aliases only; recipient/topic ids can be case-sensitive.
return stripTargetProviderPrefix(to, to.slice(0, to.indexOf(":")));
}
function normalizeTo(input: unknown): string | undefined {