From 81361755b71f88e21340c77be5a44cc85a6f479e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 14 Feb 2026 13:35:21 +0000 Subject: [PATCH] refactor(reactions): share reaction level resolver --- src/signal/reaction-level.ts | 61 ++++++-------------------- src/telegram/reaction-level.ts | 60 ++++++-------------------- src/utils/reaction-level.test.ts | 53 +++++++++++++++++++++++ src/utils/reaction-level.ts | 74 ++++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 97 deletions(-) create mode 100644 src/utils/reaction-level.test.ts create mode 100644 src/utils/reaction-level.ts diff --git a/src/signal/reaction-level.ts b/src/signal/reaction-level.ts index 5aa14b37494..f3bd2ad7454 100644 --- a/src/signal/reaction-level.ts +++ b/src/signal/reaction-level.ts @@ -1,17 +1,13 @@ import type { OpenClawConfig } from "../config/config.js"; +import { + resolveReactionLevel, + type ReactionLevel, + type ResolvedReactionLevel, +} from "../utils/reaction-level.js"; import { resolveSignalAccount } from "./accounts.js"; -export type SignalReactionLevel = "off" | "ack" | "minimal" | "extensive"; - -export type ResolvedSignalReactionLevel = { - level: SignalReactionLevel; - /** Whether ACK reactions (e.g., 👀 when processing) are enabled. */ - ackEnabled: boolean; - /** Whether agent-controlled reactions are enabled. */ - agentReactionsEnabled: boolean; - /** Guidance level for agent reactions (minimal = sparse, extensive = liberal). */ - agentReactionGuidance?: "minimal" | "extensive"; -}; +export type SignalReactionLevel = ReactionLevel; +export type ResolvedSignalReactionLevel = ResolvedReactionLevel; /** * Resolve the effective reaction level and its implications for Signal. @@ -30,42 +26,9 @@ export function resolveSignalReactionLevel(params: { cfg: params.cfg, accountId: params.accountId, }); - const level = (account.config.reactionLevel ?? "minimal") as SignalReactionLevel; - - switch (level) { - case "off": - return { - level, - ackEnabled: false, - agentReactionsEnabled: false, - }; - case "ack": - return { - level, - ackEnabled: true, - agentReactionsEnabled: false, - }; - case "minimal": - return { - level, - ackEnabled: false, - agentReactionsEnabled: true, - agentReactionGuidance: "minimal", - }; - case "extensive": - return { - level, - ackEnabled: false, - agentReactionsEnabled: true, - agentReactionGuidance: "extensive", - }; - default: - // Fallback to minimal behavior - return { - level: "minimal", - ackEnabled: false, - agentReactionsEnabled: true, - agentReactionGuidance: "minimal", - }; - } + return resolveReactionLevel({ + value: account.config.reactionLevel, + defaultLevel: "minimal", + invalidFallback: "minimal", + }); } diff --git a/src/telegram/reaction-level.ts b/src/telegram/reaction-level.ts index 2a88c573aa5..98873a05180 100644 --- a/src/telegram/reaction-level.ts +++ b/src/telegram/reaction-level.ts @@ -1,17 +1,13 @@ import type { OpenClawConfig } from "../config/config.js"; +import { + resolveReactionLevel, + type ReactionLevel, + type ResolvedReactionLevel as BaseResolvedReactionLevel, +} from "../utils/reaction-level.js"; import { resolveTelegramAccount } from "./accounts.js"; -export type TelegramReactionLevel = "off" | "ack" | "minimal" | "extensive"; - -export type ResolvedReactionLevel = { - level: TelegramReactionLevel; - /** Whether ACK reactions (e.g., 👀 when processing) are enabled. */ - ackEnabled: boolean; - /** Whether agent-controlled reactions are enabled. */ - agentReactionsEnabled: boolean; - /** Guidance level for agent reactions (minimal = sparse, extensive = liberal). */ - agentReactionGuidance?: "minimal" | "extensive"; -}; +export type TelegramReactionLevel = ReactionLevel; +export type ResolvedReactionLevel = BaseResolvedReactionLevel; /** * Resolve the effective reaction level and its implications. @@ -24,41 +20,9 @@ export function resolveTelegramReactionLevel(params: { cfg: params.cfg, accountId: params.accountId, }); - const level = (account.config.reactionLevel ?? "minimal") as TelegramReactionLevel; - - switch (level) { - case "off": - return { - level, - ackEnabled: false, - agentReactionsEnabled: false, - }; - case "ack": - return { - level, - ackEnabled: true, - agentReactionsEnabled: false, - }; - case "minimal": - return { - level, - ackEnabled: false, - agentReactionsEnabled: true, - agentReactionGuidance: "minimal", - }; - case "extensive": - return { - level, - ackEnabled: false, - agentReactionsEnabled: true, - agentReactionGuidance: "extensive", - }; - default: - // Fallback to ack behavior - return { - level: "ack", - ackEnabled: true, - agentReactionsEnabled: false, - }; - } + return resolveReactionLevel({ + value: account.config.reactionLevel, + defaultLevel: "minimal", + invalidFallback: "ack", + }); } diff --git a/src/utils/reaction-level.test.ts b/src/utils/reaction-level.test.ts new file mode 100644 index 00000000000..ade1fe5dd8c --- /dev/null +++ b/src/utils/reaction-level.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; +import { resolveReactionLevel } from "./reaction-level.js"; + +describe("resolveReactionLevel", () => { + it("defaults when value is missing", () => { + expect( + resolveReactionLevel({ value: undefined, defaultLevel: "minimal", invalidFallback: "ack" }), + ).toEqual({ + level: "minimal", + ackEnabled: false, + agentReactionsEnabled: true, + agentReactionGuidance: "minimal", + }); + }); + + it("supports ack", () => { + expect( + resolveReactionLevel({ value: "ack", defaultLevel: "minimal", invalidFallback: "ack" }), + ).toEqual({ level: "ack", ackEnabled: true, agentReactionsEnabled: false }); + }); + + it("supports extensive", () => { + expect( + resolveReactionLevel({ + value: "extensive", + defaultLevel: "minimal", + invalidFallback: "ack", + }), + ).toEqual({ + level: "extensive", + ackEnabled: false, + agentReactionsEnabled: true, + agentReactionGuidance: "extensive", + }); + }); + + it("uses invalid fallback ack", () => { + expect( + resolveReactionLevel({ value: "bogus", defaultLevel: "minimal", invalidFallback: "ack" }), + ).toEqual({ level: "ack", ackEnabled: true, agentReactionsEnabled: false }); + }); + + it("uses invalid fallback minimal", () => { + expect( + resolveReactionLevel({ value: "bogus", defaultLevel: "minimal", invalidFallback: "minimal" }), + ).toEqual({ + level: "minimal", + ackEnabled: false, + agentReactionsEnabled: true, + agentReactionGuidance: "minimal", + }); + }); +}); diff --git a/src/utils/reaction-level.ts b/src/utils/reaction-level.ts new file mode 100644 index 00000000000..f2789d26939 --- /dev/null +++ b/src/utils/reaction-level.ts @@ -0,0 +1,74 @@ +export type ReactionLevel = "off" | "ack" | "minimal" | "extensive"; + +export type ResolvedReactionLevel = { + level: ReactionLevel; + /** Whether ACK reactions (e.g., 👀 when processing) are enabled. */ + ackEnabled: boolean; + /** Whether agent-controlled reactions are enabled. */ + agentReactionsEnabled: boolean; + /** Guidance level for agent reactions (minimal = sparse, extensive = liberal). */ + agentReactionGuidance?: "minimal" | "extensive"; +}; + +const LEVELS = new Set(["off", "ack", "minimal", "extensive"]); + +function parseLevel( + value: unknown, +): { kind: "missing" } | { kind: "invalid" } | { kind: "ok"; value: ReactionLevel } { + if (value === undefined || value === null) { + return { kind: "missing" }; + } + if (typeof value !== "string") { + return { kind: "invalid" }; + } + const trimmed = value.trim(); + if (!trimmed) { + return { kind: "missing" }; + } + if (LEVELS.has(trimmed as ReactionLevel)) { + return { kind: "ok", value: trimmed as ReactionLevel }; + } + return { kind: "invalid" }; +} + +export function resolveReactionLevel(params: { + value: unknown; + defaultLevel: ReactionLevel; + invalidFallback: "ack" | "minimal"; +}): ResolvedReactionLevel { + const parsed = parseLevel(params.value); + const effective = + parsed.kind === "ok" + ? parsed.value + : parsed.kind === "missing" + ? params.defaultLevel + : params.invalidFallback; + + switch (effective) { + case "off": + return { level: "off", ackEnabled: false, agentReactionsEnabled: false }; + case "ack": + return { level: "ack", ackEnabled: true, agentReactionsEnabled: false }; + case "minimal": + return { + level: "minimal", + ackEnabled: false, + agentReactionsEnabled: true, + agentReactionGuidance: "minimal", + }; + case "extensive": + return { + level: "extensive", + ackEnabled: false, + agentReactionsEnabled: true, + agentReactionGuidance: "extensive", + }; + default: + return { + level: "minimal", + ackEnabled: false, + agentReactionsEnabled: true, + agentReactionGuidance: "minimal", + }; + } +}