fix(commands): enforce reject-vs-passthrough command policy

This commit is contained in:
mingmxren 2026-03-01 15:34:22 +08:00
parent bb4a0dc766
commit 33ba65fc91
3 changed files with 153 additions and 51 deletions

View file

@ -75,29 +75,7 @@ func builtinDefinitions(cfg *config.Config, runtime Runtime) []Definition {
Usage: "/show [model|channel]",
Channels: []string{"telegram"},
Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
if cfg == nil {
return req.Reply("Command unavailable in current context.")
}
args := commandArgs(req.Text)
if args == "" {
return req.Reply("Usage: /show [model|channel]")
}
switch args {
case "model":
return req.Reply(fmt.Sprintf(
"Current Model: %s (Provider: %s)",
cfg.Agents.Defaults.GetModelName(),
cfg.Agents.Defaults.Provider,
))
case "channel":
return req.Reply(fmt.Sprintf("Current Channel: %s", req.Channel))
default:
return req.Reply(fmt.Sprintf("Unknown parameter: %s. Try 'model' or 'channel'.", args))
}
return handleShowCommand(req, cfg)
},
},
{
@ -106,34 +84,7 @@ func builtinDefinitions(cfg *config.Config, runtime Runtime) []Definition {
Usage: "/list [models|channels]",
Channels: []string{"telegram"},
Handler: func(_ context.Context, req Request) error {
if req.Reply == nil {
return nil
}
if cfg == nil {
return req.Reply("Command unavailable in current context.")
}
args := commandArgs(req.Text)
if args == "" {
return req.Reply("Usage: /list [models|channels]")
}
switch args {
case "models":
provider := cfg.Agents.Defaults.Provider
if provider == "" {
provider = "configured default"
}
return req.Reply(fmt.Sprintf(
"Configured Model: %s\nProvider: %s\n\nTo change models, update config.json",
cfg.Agents.Defaults.GetModelName(),
provider,
))
case "channels":
enabled := enabledChannels(cfg)
return req.Reply(fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- ")))
default:
return req.Reply(fmt.Sprintf("Unknown parameter: %s. Try 'models' or 'channels'.", args))
}
return handleListCommand(req, cfg)
},
},
}
@ -262,6 +213,59 @@ func handleSessionCommand(req Request, runtime Runtime) error {
}
}
func handleShowCommand(req Request, cfg *config.Config) error {
if cfg == nil {
return reply(req, "Command unavailable in current context.")
}
args := commandArgs(req.Text)
if args == "" {
return reply(req, "Usage: /show [model|channel]")
}
switch args {
case "model":
return reply(req, fmt.Sprintf(
"Current Model: %s (Provider: %s)",
cfg.Agents.Defaults.GetModelName(),
cfg.Agents.Defaults.Provider,
))
case "channel":
return reply(req, fmt.Sprintf("Current Channel: %s", req.Channel))
default:
return reply(req, fmt.Sprintf("Unknown parameter: %s. Try 'model' or 'channel'.", args))
}
}
func handleListCommand(req Request, cfg *config.Config) error {
if cfg == nil {
return reply(req, "Command unavailable in current context.")
}
args := commandArgs(req.Text)
if args == "" {
return reply(req, "Usage: /list [models|channels]")
}
switch args {
case "models":
provider := cfg.Agents.Defaults.Provider
if provider == "" {
provider = "configured default"
}
return reply(req, fmt.Sprintf(
"Configured Model: %s\nProvider: %s\n\nTo change models, update config.json",
cfg.Agents.Defaults.GetModelName(),
provider,
))
case "channels":
enabled := enabledChannels(cfg)
return reply(req, fmt.Sprintf("Enabled Channels:\n- %s", strings.Join(enabled, "\n- ")))
default:
return reply(req, fmt.Sprintf("Unknown parameter: %s. Try 'models' or 'channels'.", args))
}
}
func reply(req Request, text string) error {
if req.Reply == nil {
return nil

View file

@ -101,3 +101,25 @@ func TestBuiltinDefinitionsWithRuntime_EnablesSessionHandlers(t *testing.T) {
t.Fatalf("/session should provide runtime-backed handler when runtime is available")
}
}
func TestBuiltinDefinitions_ShowAndListAreTelegramOnlyHandlers(t *testing.T) {
defs := BuiltinDefinitions(&config.Config{})
defByName := map[string]Definition{}
for _, def := range defs {
defByName[def.Name] = def
}
for _, name := range []string{"show", "list"} {
def, ok := defByName[name]
if !ok {
t.Fatalf("missing /%s definition", name)
}
if def.Handler == nil {
t.Fatalf("/%s should provide a builtin handler", name)
}
if len(def.Channels) != 1 || def.Channels[0] != "telegram" {
t.Fatalf("/%s channels=%v, want [telegram]", name, def.Channels)
}
}
}

View file

@ -0,0 +1,76 @@
package commands
import (
"context"
"testing"
"github.com/sipeed/picoclaw/pkg/config"
)
func TestShowListHandlers_ChannelPolicy(t *testing.T) {
cfg := &config.Config{}
ex := NewExecutor(NewRegistry(BuiltinDefinitions(cfg)))
var telegramReply string
handled := ex.Execute(context.Background(), Request{
Channel: "telegram",
Text: "/show channel",
Reply: func(text string) error {
telegramReply = text
return nil
},
})
if handled.Outcome != OutcomeHandled {
t.Fatalf("telegram /show outcome=%v, want=%v", handled.Outcome, OutcomeHandled)
}
if telegramReply != "Current Channel: telegram" {
t.Fatalf("telegram /show reply=%q, want=%q", telegramReply, "Current Channel: telegram")
}
rejected := ex.Execute(context.Background(), Request{
Channel: "whatsapp",
Text: "/show channel",
})
if rejected.Outcome != OutcomeRejected {
t.Fatalf("whatsapp /show outcome=%v, want=%v", rejected.Outcome, OutcomeRejected)
}
if rejected.Command != "show" {
t.Fatalf("whatsapp /show command=%q, want=%q", rejected.Command, "show")
}
if rejected.Reply != "Command /show is not supported on whatsapp." {
t.Fatalf("whatsapp /show reply=%q, want=%q", rejected.Reply, "Command /show is not supported on whatsapp.")
}
passthrough := ex.Execute(context.Background(), Request{
Channel: "whatsapp",
Text: "/foo",
})
if passthrough.Outcome != OutcomePassthrough {
t.Fatalf("whatsapp /foo outcome=%v, want=%v", passthrough.Outcome, OutcomePassthrough)
}
if passthrough.Command != "foo" {
t.Fatalf("whatsapp /foo command=%q, want=%q", passthrough.Command, "foo")
}
if passthrough.Reply != "" {
t.Fatalf("whatsapp /foo reply=%q, want empty", passthrough.Reply)
}
}
func TestShowListHandlers_ListRejectsUnsupportedChannel(t *testing.T) {
cfg := &config.Config{}
ex := NewExecutor(NewRegistry(BuiltinDefinitions(cfg)))
res := ex.Execute(context.Background(), Request{
Channel: "whatsapp",
Text: "/list channels",
})
if res.Outcome != OutcomeRejected {
t.Fatalf("whatsapp /list outcome=%v, want=%v", res.Outcome, OutcomeRejected)
}
if res.Command != "list" {
t.Fatalf("whatsapp /list command=%q, want=%q", res.Command, "list")
}
if res.Reply != "Command /list is not supported on whatsapp." {
t.Fatalf("whatsapp /list reply=%q, want=%q", res.Reply, "Command /list is not supported on whatsapp.")
}
}