From f46bcbe16d3329c7b431ec0e057d99521387f87b Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 18 Feb 2026 17:29:24 +0000 Subject: [PATCH] refactor(auto-reply): share slash set/unset command parsing --- .../reply/commands-setunset.test.ts | 63 ++++++++++++++++++- src/auto-reply/reply/commands-setunset.ts | 39 ++++++++++++ src/auto-reply/reply/config-commands.ts | 41 ++++-------- src/auto-reply/reply/debug-commands.ts | 44 +++++-------- 4 files changed, 126 insertions(+), 61 deletions(-) diff --git a/src/auto-reply/reply/commands-setunset.test.ts b/src/auto-reply/reply/commands-setunset.test.ts index 5cac023175..7e471a4a83 100644 --- a/src/auto-reply/reply/commands-setunset.test.ts +++ b/src/auto-reply/reply/commands-setunset.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from "vitest"; -import { parseSetUnsetCommand, parseSetUnsetCommandAction } from "./commands-setunset.js"; +import { + parseSetUnsetCommand, + parseSetUnsetCommandAction, + parseSlashCommandWithSetUnset, +} from "./commands-setunset.js"; type ParsedSetUnsetAction = | { action: "set"; path: string; value: unknown } @@ -53,3 +57,60 @@ describe("parseSetUnsetCommandAction", () => { expect(result).toEqual({ action: "error", message: "Usage: /config set path=value" }); }); }); + +describe("parseSlashCommandWithSetUnset", () => { + it("returns null when the input does not match the slash command", () => { + const result = parseSlashCommandWithSetUnset({ + raw: "/debug show", + slash: "/config", + invalidMessage: "Invalid /config syntax.", + usageMessage: "Usage: /config show|set|unset", + onKnownAction: () => undefined, + onSet: (path, value) => ({ action: "set", path, value }), + onUnset: (path) => ({ action: "unset", path }), + onError: (message) => ({ action: "error", message }), + }); + expect(result).toBeNull(); + }); + + it("prefers set/unset mapping and falls back to known actions", () => { + const setResult = parseSlashCommandWithSetUnset({ + raw: '/config set a.b={"ok":true}', + slash: "/config", + invalidMessage: "Invalid /config syntax.", + usageMessage: "Usage: /config show|set|unset", + onKnownAction: () => undefined, + onSet: (path, value) => ({ action: "set", path, value }), + onUnset: (path) => ({ action: "unset", path }), + onError: (message) => ({ action: "error", message }), + }); + expect(setResult).toEqual({ action: "set", path: "a.b", value: { ok: true } }); + + const showResult = parseSlashCommandWithSetUnset({ + raw: "/config show", + slash: "/config", + invalidMessage: "Invalid /config syntax.", + usageMessage: "Usage: /config show|set|unset", + onKnownAction: (action) => + action === "show" ? { action: "unset", path: "dummy" } : undefined, + onSet: (path, value) => ({ action: "set", path, value }), + onUnset: (path) => ({ action: "unset", path }), + onError: (message) => ({ action: "error", message }), + }); + expect(showResult).toEqual({ action: "unset", path: "dummy" }); + }); + + it("returns onError for unknown actions", () => { + const unknownAction = parseSlashCommandWithSetUnset({ + raw: "/config whoami", + slash: "/config", + invalidMessage: "Invalid /config syntax.", + usageMessage: "Usage: /config show|set|unset", + onKnownAction: () => undefined, + onSet: (path, value) => ({ action: "set", path, value }), + onUnset: (path) => ({ action: "unset", path }), + onError: (message) => ({ action: "error", message }), + }); + expect(unknownAction).toEqual({ action: "error", message: "Usage: /config show|set|unset" }); + }); +}); diff --git a/src/auto-reply/reply/commands-setunset.ts b/src/auto-reply/reply/commands-setunset.ts index ee0f3fe797..2f971d6765 100644 --- a/src/auto-reply/reply/commands-setunset.ts +++ b/src/auto-reply/reply/commands-setunset.ts @@ -1,3 +1,4 @@ +import { parseSlashCommandOrNull } from "./commands-slash-parse.js"; import { parseConfigValue } from "./config-value.js"; export type SetUnsetParseResult = @@ -60,3 +61,41 @@ export function parseSetUnsetCommandAction(params: { ? params.onSet(parsed.path, parsed.value) : params.onUnset(parsed.path); } + +export function parseSlashCommandWithSetUnset(params: { + raw: string; + slash: string; + invalidMessage: string; + usageMessage: string; + onKnownAction: (action: string, args: string) => T | undefined; + onSet: (path: string, value: unknown) => T; + onUnset: (path: string) => T; + onError: (message: string) => T; +}): T | null { + const parsed = parseSlashCommandOrNull(params.raw, params.slash, { + invalidMessage: params.invalidMessage, + }); + if (!parsed) { + return null; + } + if (!parsed.ok) { + return params.onError(parsed.message); + } + const { action, args } = parsed; + const setUnset = parseSetUnsetCommandAction({ + slash: params.slash, + action, + args, + onSet: params.onSet, + onUnset: params.onUnset, + onError: params.onError, + }); + if (setUnset) { + return setUnset; + } + const knownAction = params.onKnownAction(action, args); + if (knownAction) { + return knownAction; + } + return params.onError(params.usageMessage); +} diff --git a/src/auto-reply/reply/config-commands.ts b/src/auto-reply/reply/config-commands.ts index b864f876ab..888d26ac8b 100644 --- a/src/auto-reply/reply/config-commands.ts +++ b/src/auto-reply/reply/config-commands.ts @@ -1,5 +1,4 @@ -import { parseSetUnsetCommandAction } from "./commands-setunset.js"; -import { parseSlashCommandOrNull } from "./commands-slash-parse.js"; +import { parseSlashCommandWithSetUnset } from "./commands-setunset.js"; export type ConfigCommand = | { action: "show"; path?: string } @@ -8,37 +7,19 @@ export type ConfigCommand = | { action: "error"; message: string }; export function parseConfigCommand(raw: string): ConfigCommand | null { - const parsed = parseSlashCommandOrNull(raw, "/config", { - invalidMessage: "Invalid /config syntax.", - }); - if (!parsed) { - return null; - } - if (!parsed.ok) { - return { action: "error", message: parsed.message }; - } - const { action, args } = parsed; - const setUnset = parseSetUnsetCommandAction({ + return parseSlashCommandWithSetUnset({ + raw, slash: "/config", - action, - args, + invalidMessage: "Invalid /config syntax.", + usageMessage: "Usage: /config show|set|unset", + onKnownAction: (action, args) => { + if (action === "show" || action === "get") { + return { action: "show", path: args || undefined }; + } + return undefined; + }, onSet: (path, value) => ({ action: "set", path, value }), onUnset: (path) => ({ action: "unset", path }), onError: (message) => ({ action: "error", message }), }); - if (setUnset) { - return setUnset; - } - - switch (action) { - case "show": - return { action: "show", path: args || undefined }; - case "get": - return { action: "show", path: args || undefined }; - default: - return { - action: "error", - message: "Usage: /config show|set|unset", - }; - } } diff --git a/src/auto-reply/reply/debug-commands.ts b/src/auto-reply/reply/debug-commands.ts index 89df54201a..9d06c16d5f 100644 --- a/src/auto-reply/reply/debug-commands.ts +++ b/src/auto-reply/reply/debug-commands.ts @@ -1,5 +1,4 @@ -import { parseSetUnsetCommandAction } from "./commands-setunset.js"; -import { parseSlashCommandOrNull } from "./commands-slash-parse.js"; +import { parseSlashCommandWithSetUnset } from "./commands-setunset.js"; export type DebugCommand = | { action: "show" } @@ -9,37 +8,22 @@ export type DebugCommand = | { action: "error"; message: string }; export function parseDebugCommand(raw: string): DebugCommand | null { - const parsed = parseSlashCommandOrNull(raw, "/debug", { - invalidMessage: "Invalid /debug syntax.", - }); - if (!parsed) { - return null; - } - if (!parsed.ok) { - return { action: "error", message: parsed.message }; - } - const { action, args } = parsed; - const setUnset = parseSetUnsetCommandAction({ + return parseSlashCommandWithSetUnset({ + raw, slash: "/debug", - action, - args, + invalidMessage: "Invalid /debug syntax.", + usageMessage: "Usage: /debug show|set|unset|reset", + onKnownAction: (action) => { + if (action === "show") { + return { action: "show" }; + } + if (action === "reset") { + return { action: "reset" }; + } + return undefined; + }, onSet: (path, value) => ({ action: "set", path, value }), onUnset: (path) => ({ action: "unset", path }), onError: (message) => ({ action: "error", message }), }); - if (setUnset) { - return setUnset; - } - - switch (action) { - case "show": - return { action: "show" }; - case "reset": - return { action: "reset" }; - default: - return { - action: "error", - message: "Usage: /debug show|set|unset|reset", - }; - } }