refactor(auto-reply): share slash set/unset command parsing

This commit is contained in:
Peter Steinberger
2026-02-18 17:29:24 +00:00
parent fedebc245e
commit f46bcbe16d
4 changed files with 126 additions and 61 deletions

View File

@@ -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<ParsedSetUnsetAction>({
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<ParsedSetUnsetAction>({
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<ParsedSetUnsetAction>({
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<ParsedSetUnsetAction>({
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" });
});
});

View File

@@ -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<T>(params: {
? params.onSet(parsed.path, parsed.value)
: params.onUnset(parsed.path);
}
export function parseSlashCommandWithSetUnset<T>(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<T>({
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);
}

View File

@@ -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<ConfigCommand>({
return parseSlashCommandWithSetUnset<ConfigCommand>({
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",
};
}
}

View File

@@ -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<DebugCommand>({
return parseSlashCommandWithSetUnset<DebugCommand>({
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",
};
}
}