mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-05 18:52:57 +00:00
fix(cli): validate message numeric options
This commit is contained in:
@@ -306,6 +306,116 @@ describe("runMessageAction", () => {
|
||||
expect(exitMock).not.toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"poll duration hours",
|
||||
"poll",
|
||||
{
|
||||
channel: "discord",
|
||||
target: "123",
|
||||
pollQuestion: "ship?",
|
||||
pollOption: ["yes", "no"],
|
||||
pollDurationHours: "1.5",
|
||||
},
|
||||
"--poll-duration-hours",
|
||||
],
|
||||
[
|
||||
"poll duration seconds",
|
||||
"poll",
|
||||
{
|
||||
channel: "telegram",
|
||||
target: "123",
|
||||
pollQuestion: "ship?",
|
||||
pollOption: ["yes", "no"],
|
||||
pollDurationSeconds: "60s",
|
||||
},
|
||||
"--poll-duration-seconds",
|
||||
],
|
||||
[
|
||||
"timeout duration",
|
||||
"timeout",
|
||||
{ guildId: "g", userId: "u", durationMin: "5m" },
|
||||
"--duration-min",
|
||||
],
|
||||
["ban delete days", "ban", { guildId: "g", userId: "u", deleteDays: "7d" }, "--delete-days"],
|
||||
["read limit", "read", { channel: "discord", target: "123", limit: "10x" }, "--limit"],
|
||||
["search limit", "search", { guildId: "g", query: "hello", limit: "10x" }, "--limit"],
|
||||
["pins limit", "list-pins", { channel: "discord", target: "123", limit: "10x" }, "--limit"],
|
||||
[
|
||||
"reactions limit",
|
||||
"reactions",
|
||||
{ channel: "discord", target: "123", messageId: "m", limit: "10x" },
|
||||
"--limit",
|
||||
],
|
||||
[
|
||||
"thread auto archive minutes",
|
||||
"thread-create",
|
||||
{
|
||||
channel: "discord",
|
||||
target: "123",
|
||||
threadName: "ops",
|
||||
autoArchiveMin: "60m",
|
||||
},
|
||||
"--auto-archive-min",
|
||||
],
|
||||
["thread list limit", "thread-list", { guildId: "g", limit: "10x" }, "--limit"],
|
||||
])("rejects malformed numeric CLI option for %s", async (_name, action, opts, flag) => {
|
||||
const runMessageAction = createRunMessageAction();
|
||||
|
||||
await expect(runMessageAction(action, opts)).rejects.toThrow("exit");
|
||||
|
||||
const kind = flag === "--delete-days" ? "non-negative" : "positive";
|
||||
expect(errorMock).toHaveBeenCalledWith(`Error: ${flag} must be a ${kind} integer.`);
|
||||
expect(ensurePluginRegistryLoaded).not.toHaveBeenCalled();
|
||||
expect(messageCommandMock).not.toHaveBeenCalled();
|
||||
expect(exitMock).toHaveBeenCalledWith(1);
|
||||
expect(exitMock).not.toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["pollDurationHours", "0", "--poll-duration-hours"],
|
||||
["pollDurationSeconds", "-1", "--poll-duration-seconds"],
|
||||
["durationMin", "", "--duration-min"],
|
||||
["deleteDays", Number.NaN, "--delete-days"],
|
||||
["limit", 1.2, "--limit"],
|
||||
["autoArchiveMin", null, "--auto-archive-min"],
|
||||
])("rejects non-positive or non-integer %s values", async (key, value, flag) => {
|
||||
const runMessageAction = createRunMessageAction();
|
||||
|
||||
await expect(
|
||||
runMessageAction("send", {
|
||||
...baseSendOptions,
|
||||
[key]: value,
|
||||
}),
|
||||
).rejects.toThrow("exit");
|
||||
|
||||
const kind = flag === "--delete-days" ? "non-negative" : "positive";
|
||||
expect(errorMock).toHaveBeenCalledWith(`Error: ${flag} must be a ${kind} integer.`);
|
||||
expect(messageCommandMock).not.toHaveBeenCalled();
|
||||
expect(exitMock).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("allows zero delete-days for no-history Discord bans", async () => {
|
||||
const runMessageAction = createRunMessageAction();
|
||||
|
||||
await expect(
|
||||
runMessageAction("ban", {
|
||||
guildId: "g",
|
||||
userId: "u",
|
||||
deleteDays: "0",
|
||||
}),
|
||||
).rejects.toThrow("exit");
|
||||
|
||||
expect(errorMock).not.toHaveBeenCalled();
|
||||
expectMessageCommandOptions({
|
||||
action: "ban",
|
||||
guildId: "g",
|
||||
userId: "u",
|
||||
deleteDays: "0",
|
||||
});
|
||||
expect(exitMock).toHaveBeenCalledWith(0);
|
||||
});
|
||||
|
||||
it("runs gateway_stop hooks before exit when registered", async () => {
|
||||
hasHooksMock.mockReturnValueOnce(true);
|
||||
await runSendAction();
|
||||
|
||||
@@ -8,6 +8,10 @@ import { resolveMessageSecretScope } from "../../../cli/message-secret-scope.js"
|
||||
import { messageCommand } from "../../../commands/message.js";
|
||||
import { danger, setVerbose } from "../../../globals.js";
|
||||
import { CHANNEL_TARGET_DESCRIPTION } from "../../../infra/outbound/channel-target.js";
|
||||
import {
|
||||
parseStrictNonNegativeInteger,
|
||||
parseStrictPositiveInteger,
|
||||
} from "../../../infra/parse-finite-number.js";
|
||||
import { runGlobalGatewayStopSafely } from "../../../plugins/hook-runner-global.js";
|
||||
import { defaultRuntime } from "../../../runtime.js";
|
||||
import { runCommandWithRuntime } from "../../cli-utils.js";
|
||||
@@ -25,6 +29,14 @@ const GATEWAY_STOP_TIMEOUT_MS = 2500;
|
||||
const ACTIONS_WITHOUT_STOP_HOOKS = new Set(["read"]);
|
||||
const ACTIONS_REQUIRING_CONFIGURED_CHANNEL_PRELOAD = new Set(["broadcast"]);
|
||||
const CHANNEL_MESSAGE_ACTION_NAME_SET = new Set<string>(CHANNEL_MESSAGE_ACTION_NAMES);
|
||||
const STRICT_POSITIVE_INTEGER_OPTIONS = new Map([
|
||||
["pollDurationHours", "--poll-duration-hours"],
|
||||
["pollDurationSeconds", "--poll-duration-seconds"],
|
||||
["durationMin", "--duration-min"],
|
||||
["limit", "--limit"],
|
||||
["autoArchiveMin", "--auto-archive-min"],
|
||||
]);
|
||||
const STRICT_NON_NEGATIVE_INTEGER_OPTIONS = new Map([["deleteDays", "--delete-days"]]);
|
||||
|
||||
type MessagePluginLoadOptions = { scope: PluginRegistryScope; onlyChannelIds?: string[] };
|
||||
type MessagePluginPreloadPlan =
|
||||
@@ -39,6 +51,25 @@ function normalizeMessageOptions(opts: Record<string, unknown>): Record<string,
|
||||
};
|
||||
}
|
||||
|
||||
function validateMessageNumericOptions(opts: Record<string, unknown>): void {
|
||||
for (const [key, flag] of STRICT_POSITIVE_INTEGER_OPTIONS) {
|
||||
if (opts[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (parseStrictPositiveInteger(opts[key]) === undefined) {
|
||||
throw new Error(`${flag} must be a positive integer.`);
|
||||
}
|
||||
}
|
||||
for (const [key, flag] of STRICT_NON_NEGATIVE_INTEGER_OPTIONS) {
|
||||
if (opts[key] === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (parseStrictNonNegativeInteger(opts[key]) === undefined) {
|
||||
throw new Error(`${flag} must be a non-negative integer.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runPluginStopHooks(): Promise<void> {
|
||||
let timeout: NodeJS.Timeout | null = null;
|
||||
const hookRun = runGlobalGatewayStopSafely({
|
||||
@@ -128,6 +159,7 @@ export function createMessageCliHelpers(
|
||||
await runCommandWithRuntime(
|
||||
defaultRuntime,
|
||||
async () => {
|
||||
validateMessageNumericOptions(opts);
|
||||
const preloadPlan = resolveMessagePluginPreloadPlan(action, opts);
|
||||
if (preloadPlan.preload) {
|
||||
ensurePluginRegistryLoaded(preloadPlan.loadOptions);
|
||||
|
||||
Reference in New Issue
Block a user