mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-07 14:22:56 +00:00
`parseSlashCommandActionArgs` used a naive `startsWith` against the configured slash prefix. When a skill name shares a prefix with a built-in command (e.g. a skill named `config-check` vs the built-in `/config`), the longer name was captured by the shorter built-in handler and surfaced as an invalid action: ⚠️ /config is disabled. Set commands.config=true to enable. Any skill whose name starts with a built-in command prefix (`config-*`, `debug-*`, `models-*`, etc.) was unreachable via slash invocation from any channel. Fix: after the prefix match, require that the next character is whitespace, a colon, or end-of-string. Otherwise the prefix collided with a longer command name and we return `no-match` so the longer handler — or the skill router — gets a chance to claim it. Adds a regression test file `commands-slash-parse.test.ts` covering: - `/config-check <args>` returns null (the reported case) - `/configfoo` (no separator) returns null - `/modelsy` returns null for the `/models` prefix - `/config:json` still matches (colon is a valid boundary) - `/config show enabled` still parses cleanly (whitespace boundary) - empty body still returns the default action Fixes #84572. Co-authored-by: infracore <infracore@users.noreply.github.com>
54 lines
2.4 KiB
TypeScript
54 lines
2.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import { parseSlashCommandOrNull } from "./commands-slash-parse.js";
|
|
|
|
describe("parseSlashCommandOrNull", () => {
|
|
const opts = { invalidMessage: "invalid" };
|
|
|
|
it("returns null when the input doesn't start with the slash prefix", () => {
|
|
expect(parseSlashCommandOrNull("hello world", "/config", opts)).toBeNull();
|
|
});
|
|
|
|
it("parses action + args when the input has a clean word boundary", () => {
|
|
const result = parseSlashCommandOrNull("/config show enabled", "/config", opts);
|
|
expect(result).toEqual({ ok: true, action: "show", args: "enabled" });
|
|
});
|
|
|
|
it("returns the default action on an empty body", () => {
|
|
const result = parseSlashCommandOrNull("/config", "/config", { ...opts, defaultAction: "show" });
|
|
expect(result).toEqual({ ok: true, action: "show", args: "" });
|
|
});
|
|
|
|
describe("regression: #84572 — prefix match must require a word boundary", () => {
|
|
// Previously, `/config-check <args>` matched the `/config` handler
|
|
// via a naive `startsWith` and surfaced as an invalid action, blocking
|
|
// any skill whose name shared a prefix with a built-in command.
|
|
it("does not match a longer command name with a hyphen tail (`/config-check`)", () => {
|
|
expect(parseSlashCommandOrNull("/config-check arg1 arg2", "/config", opts)).toBeNull();
|
|
});
|
|
|
|
it("does not match a longer command name with no whitespace after prefix", () => {
|
|
expect(parseSlashCommandOrNull("/configfoo", "/config", opts)).toBeNull();
|
|
});
|
|
|
|
it("does not match when prefix sits in the middle of a longer word", () => {
|
|
// /modelsy should not be captured by /models
|
|
expect(parseSlashCommandOrNull("/modelsy", "/models", opts)).toBeNull();
|
|
});
|
|
|
|
it("still matches when the boundary is a colon (`/config:json`)", () => {
|
|
// Some clients allow `cmd:subkey` to pass through to the action parser
|
|
// when there's no whitespace — the boundary character is still a
|
|
// separator and not an alpha continuation.
|
|
const result = parseSlashCommandOrNull("/config:json", "/config", opts);
|
|
expect(result).not.toBeNull();
|
|
expect(result?.ok).toBe(true);
|
|
});
|
|
|
|
it("still matches the exact prefix with leading whitespace", () => {
|
|
const result = parseSlashCommandOrNull(" /config show ", "/config", opts);
|
|
expect(result).toEqual({ ok: true, action: "show", args: "" });
|
|
});
|
|
});
|
|
});
|