From 86b519850e330ee5da02bee359e9ea021d08ae32 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 1 Apr 2026 16:22:06 +0900 Subject: [PATCH] refactor: consolidate cron delivery boundary parsing --- src/cron/delivery-field-schemas.ts | 79 ++++++++++++++++++ src/cron/legacy-delivery.ts | 110 ++++++++---------------- src/cron/normalize.ts | 130 +++++++++++------------------ 3 files changed, 163 insertions(+), 156 deletions(-) create mode 100644 src/cron/delivery-field-schemas.ts diff --git a/src/cron/delivery-field-schemas.ts b/src/cron/delivery-field-schemas.ts new file mode 100644 index 00000000000..21e7aaf1013 --- /dev/null +++ b/src/cron/delivery-field-schemas.ts @@ -0,0 +1,79 @@ +import { z, type ZodType } from "zod"; + +const trimStringPreprocess = (value: unknown) => (typeof value === "string" ? value.trim() : value); + +const trimLowercaseStringPreprocess = (value: unknown) => + typeof value === "string" ? value.trim().toLowerCase() : value; + +export const DeliveryModeFieldSchema = z + .preprocess(trimLowercaseStringPreprocess, z.enum(["deliver", "announce", "none", "webhook"])) + .transform((value) => (value === "deliver" ? "announce" : value)); + +export const LowercaseNonEmptyStringFieldSchema = z.preprocess( + trimLowercaseStringPreprocess, + z.string().min(1), +); + +export const TrimmedNonEmptyStringFieldSchema = z.preprocess( + trimStringPreprocess, + z.string().min(1), +); + +export const DeliveryThreadIdFieldSchema = z.union([ + TrimmedNonEmptyStringFieldSchema, + z.number().finite(), +]); + +export const LegacyDeliveryThreadIdFieldSchema = DeliveryThreadIdFieldSchema.transform((value) => + String(value), +); + +export const TimeoutSecondsFieldSchema = z + .number() + .finite() + .transform((value) => Math.max(0, Math.floor(value))); + +export type ParsedDeliveryInput = { + mode?: "announce" | "none" | "webhook"; + channel?: string; + to?: string; + threadId?: string | number; + accountId?: string; +}; + +export function parseDeliveryInput(input: Record): ParsedDeliveryInput { + return { + mode: parseOptionalField(DeliveryModeFieldSchema, input.mode), + channel: parseOptionalField(LowercaseNonEmptyStringFieldSchema, input.channel), + to: parseOptionalField(TrimmedNonEmptyStringFieldSchema, input.to), + threadId: parseOptionalField(DeliveryThreadIdFieldSchema, input.threadId), + accountId: parseOptionalField(TrimmedNonEmptyStringFieldSchema, input.accountId), + }; +} + +export type ParsedLegacyDeliveryHints = { + deliver?: boolean; + bestEffortDeliver?: boolean; + channel?: string; + provider?: string; + to?: string; + threadId?: string; +}; + +export function parseLegacyDeliveryHintsInput( + payload: Record, +): ParsedLegacyDeliveryHints { + return { + deliver: parseOptionalField(z.boolean(), payload.deliver), + bestEffortDeliver: parseOptionalField(z.boolean(), payload.bestEffortDeliver), + channel: parseOptionalField(LowercaseNonEmptyStringFieldSchema, payload.channel), + provider: parseOptionalField(LowercaseNonEmptyStringFieldSchema, payload.provider), + to: parseOptionalField(TrimmedNonEmptyStringFieldSchema, payload.to), + threadId: parseOptionalField(LegacyDeliveryThreadIdFieldSchema, payload.threadId), + }; +} + +export function parseOptionalField(schema: ZodType, value: unknown): T | undefined { + const parsed = schema.safeParse(value); + return parsed.success ? parsed.data : undefined; +} diff --git a/src/cron/legacy-delivery.ts b/src/cron/legacy-delivery.ts index a1dc4210ef0..b4a6eb1f64d 100644 --- a/src/cron/legacy-delivery.ts +++ b/src/cron/legacy-delivery.ts @@ -1,107 +1,71 @@ +import { parseLegacyDeliveryHintsInput } from "./delivery-field-schemas.js"; + export function hasLegacyDeliveryHints(payload: Record) { - if (typeof payload.deliver === "boolean") { - return true; - } - if (typeof payload.bestEffortDeliver === "boolean") { - return true; - } - if (typeof payload.channel === "string" && payload.channel.trim()) { - return true; - } - if (typeof payload.provider === "string" && payload.provider.trim()) { - return true; - } - if (typeof payload.to === "string" && payload.to.trim()) { - return true; - } - if (typeof payload.threadId === "string" && payload.threadId.trim()) { - return true; - } - if (typeof payload.threadId === "number" && Number.isFinite(payload.threadId)) { - return true; - } - return false; + const hints = parseLegacyDeliveryHintsInput(payload); + return ( + hints.deliver !== undefined || + hints.bestEffortDeliver !== undefined || + hints.channel !== undefined || + hints.provider !== undefined || + hints.to !== undefined || + hints.threadId !== undefined + ); } export function buildDeliveryFromLegacyPayload( payload: Record, ): Record { - const deliver = payload.deliver; - const mode = deliver === false ? "none" : "announce"; - const channelRaw = - typeof payload.channel === "string" && payload.channel.trim() - ? payload.channel.trim().toLowerCase() - : typeof payload.provider === "string" - ? payload.provider.trim().toLowerCase() - : ""; - const toRaw = typeof payload.to === "string" ? payload.to.trim() : ""; - const threadIdRaw = - typeof payload.threadId === "string" - ? payload.threadId.trim() - : typeof payload.threadId === "number" && Number.isFinite(payload.threadId) - ? String(payload.threadId) - : ""; + const hints = parseLegacyDeliveryHintsInput(payload); + const mode = hints.deliver === false ? "none" : "announce"; const next: Record = { mode }; - if (channelRaw) { - next.channel = channelRaw; + if (hints.channel ?? hints.provider) { + next.channel = hints.channel ?? hints.provider; } - if (toRaw) { - next.to = toRaw; + if (hints.to) { + next.to = hints.to; } - if (threadIdRaw) { - next.threadId = threadIdRaw; + if (hints.threadId) { + next.threadId = hints.threadId; } - if (typeof payload.bestEffortDeliver === "boolean") { - next.bestEffort = payload.bestEffortDeliver; + if (hints.bestEffortDeliver !== undefined) { + next.bestEffort = hints.bestEffortDeliver; } return next; } export function buildDeliveryPatchFromLegacyPayload(payload: Record) { - const deliver = payload.deliver; - const channelRaw = - typeof payload.channel === "string" && payload.channel.trim() - ? payload.channel.trim().toLowerCase() - : typeof payload.provider === "string" && payload.provider.trim() - ? payload.provider.trim().toLowerCase() - : ""; - const toRaw = typeof payload.to === "string" ? payload.to.trim() : ""; - const threadIdRaw = - typeof payload.threadId === "string" - ? payload.threadId.trim() - : typeof payload.threadId === "number" && Number.isFinite(payload.threadId) - ? String(payload.threadId) - : ""; + const hints = parseLegacyDeliveryHintsInput(payload); const next: Record = {}; let hasPatch = false; - if (deliver === false) { + if (hints.deliver === false) { next.mode = "none"; hasPatch = true; } else if ( - deliver === true || - channelRaw || - toRaw || - threadIdRaw || - typeof payload.bestEffortDeliver === "boolean" + hints.deliver === true || + hints.channel || + hints.provider || + hints.to || + hints.threadId || + hints.bestEffortDeliver !== undefined ) { next.mode = "announce"; hasPatch = true; } - if (channelRaw) { - next.channel = channelRaw; + if (hints.channel ?? hints.provider) { + next.channel = hints.channel ?? hints.provider; hasPatch = true; } - if (toRaw) { - next.to = toRaw; + if (hints.to) { + next.to = hints.to; hasPatch = true; } - if (threadIdRaw) { - next.threadId = threadIdRaw; + if (hints.threadId) { + next.threadId = hints.threadId; hasPatch = true; } - if (typeof payload.bestEffortDeliver === "boolean") { - next.bestEffort = payload.bestEffortDeliver; + if (hints.bestEffortDeliver !== undefined) { + next.bestEffort = hints.bestEffortDeliver; hasPatch = true; } diff --git a/src/cron/normalize.ts b/src/cron/normalize.ts index 8990c2480a7..9f2a73d711e 100644 --- a/src/cron/normalize.ts +++ b/src/cron/normalize.ts @@ -1,5 +1,13 @@ import { sanitizeAgentId } from "../routing/session-key.js"; import { isRecord } from "../utils.js"; +import { + DeliveryThreadIdFieldSchema, + TimeoutSecondsFieldSchema, + TrimmedNonEmptyStringFieldSchema, + parseDeliveryInput, + parseLegacyDeliveryHintsInput, + parseOptionalField, +} from "./delivery-field-schemas.js"; import { normalizeLegacyDeliveryInput } from "./legacy-delivery.js"; import { parseAbsoluteTimeMs } from "./parse.js"; import { migrateLegacyCronPayload } from "./payload-migration.js"; @@ -124,32 +132,25 @@ function coercePayload(payload: UnknownRecord) { } } if ("model" in next) { - if (typeof next.model === "string") { - const trimmed = next.model.trim(); - if (trimmed) { - next.model = trimmed; - } else { - delete next.model; - } + const model = parseOptionalField(TrimmedNonEmptyStringFieldSchema, next.model); + if (model !== undefined) { + next.model = model; } else { delete next.model; } } if ("thinking" in next) { - if (typeof next.thinking === "string") { - const trimmed = next.thinking.trim(); - if (trimmed) { - next.thinking = trimmed; - } else { - delete next.thinking; - } + const thinking = parseOptionalField(TrimmedNonEmptyStringFieldSchema, next.thinking); + if (thinking !== undefined) { + next.thinking = thinking; } else { delete next.thinking; } } if ("timeoutSeconds" in next) { - if (typeof next.timeoutSeconds === "number" && Number.isFinite(next.timeoutSeconds)) { - next.timeoutSeconds = Math.max(0, Math.floor(next.timeoutSeconds)); + const timeoutSeconds = parseOptionalField(TimeoutSecondsFieldSchema, next.timeoutSeconds); + if (timeoutSeconds !== undefined) { + next.timeoutSeconds = timeoutSeconds; } else { delete next.timeoutSeconds; } @@ -180,54 +181,30 @@ function coercePayload(payload: UnknownRecord) { function coerceDelivery(delivery: UnknownRecord) { const next: UnknownRecord = { ...delivery }; - if (typeof delivery.mode === "string") { - const mode = delivery.mode.trim().toLowerCase(); - if (mode === "deliver") { - next.mode = "announce"; - } else if (mode === "announce" || mode === "none" || mode === "webhook") { - next.mode = mode; - } else { - delete next.mode; - } + const parsed = parseDeliveryInput(delivery); + if (parsed.mode !== undefined) { + next.mode = parsed.mode; } else if ("mode" in next) { delete next.mode; } - if (typeof delivery.channel === "string") { - const trimmed = delivery.channel.trim().toLowerCase(); - if (trimmed) { - next.channel = trimmed; - } else { - delete next.channel; - } + if (parsed.channel !== undefined) { + next.channel = parsed.channel; + } else if ("channel" in next) { + delete next.channel; } - if (typeof delivery.to === "string") { - const trimmed = delivery.to.trim(); - if (trimmed) { - next.to = trimmed; - } else { - delete next.to; - } + if (parsed.to !== undefined) { + next.to = parsed.to; + } else if ("to" in next) { + delete next.to; } - if (typeof delivery.threadId === "number" && Number.isFinite(delivery.threadId)) { - next.threadId = delivery.threadId; - } else if (typeof delivery.threadId === "string") { - const trimmed = delivery.threadId.trim(); - if (trimmed) { - next.threadId = trimmed; - } else { - delete next.threadId; - } + if (parsed.threadId !== undefined) { + next.threadId = parsed.threadId; } else if ("threadId" in next) { delete next.threadId; } - if (typeof delivery.accountId === "string") { - const trimmed = delivery.accountId.trim(); - if (trimmed) { - next.accountId = trimmed; - } else { - delete next.accountId; - } - } else if ("accountId" in next && typeof next.accountId !== "string") { + if (parsed.accountId !== undefined) { + next.accountId = parsed.accountId; + } else if ("accountId" in next) { delete next.accountId; } return next; @@ -298,38 +275,25 @@ function copyTopLevelAgentTurnFields(next: UnknownRecord, payload: UnknownRecord } function copyTopLevelLegacyDeliveryFields(next: UnknownRecord, payload: UnknownRecord) { - if (typeof payload.deliver !== "boolean" && typeof next.deliver === "boolean") { - payload.deliver = next.deliver; + const hints = parseLegacyDeliveryHintsInput(next); + if (typeof payload.deliver !== "boolean" && hints.deliver !== undefined) { + payload.deliver = hints.deliver; } - if ( - typeof payload.channel !== "string" && - typeof next.channel === "string" && - next.channel.trim() - ) { - payload.channel = next.channel.trim(); + if (typeof payload.channel !== "string" && hints.channel !== undefined) { + payload.channel = hints.channel; } - if (typeof payload.to !== "string" && typeof next.to === "string" && next.to.trim()) { - payload.to = next.to.trim(); + if (typeof payload.to !== "string" && hints.to !== undefined) { + payload.to = hints.to; } - if ( - !("threadId" in payload) && - ((typeof next.threadId === "number" && Number.isFinite(next.threadId)) || - (typeof next.threadId === "string" && next.threadId.trim())) - ) { - payload.threadId = typeof next.threadId === "string" ? next.threadId.trim() : next.threadId; + const threadId = parseOptionalField(DeliveryThreadIdFieldSchema, next.threadId); + if (!("threadId" in payload) && threadId !== undefined) { + payload.threadId = threadId; } - if ( - typeof payload.bestEffortDeliver !== "boolean" && - typeof next.bestEffortDeliver === "boolean" - ) { - payload.bestEffortDeliver = next.bestEffortDeliver; + if (typeof payload.bestEffortDeliver !== "boolean" && hints.bestEffortDeliver !== undefined) { + payload.bestEffortDeliver = hints.bestEffortDeliver; } - if ( - typeof payload.provider !== "string" && - typeof next.provider === "string" && - next.provider.trim() - ) { - payload.provider = next.provider.trim(); + if (typeof payload.provider !== "string" && hints.provider !== undefined) { + payload.provider = hints.provider; } }