From 0844cbbce47a172bacc4ebc8e07e6b99a21d7b02 Mon Sep 17 00:00:00 2001 From: mingmxren Date: Sun, 1 Mar 2026 14:50:34 +0800 Subject: [PATCH] feat(commands): add tri-state command executor contract --- pkg/commands/executor.go | 71 +++++++++++++++++++++++++++++++++++ pkg/commands/executor_test.go | 49 ++++++++++++++++++++++++ 2 files changed, 120 insertions(+) create mode 100644 pkg/commands/executor.go create mode 100644 pkg/commands/executor_test.go diff --git a/pkg/commands/executor.go b/pkg/commands/executor.go new file mode 100644 index 000000000..29a62e08c --- /dev/null +++ b/pkg/commands/executor.go @@ -0,0 +1,71 @@ +package commands + +import ( + "context" + "fmt" +) + +type Outcome int + +const ( + OutcomePassthrough Outcome = iota + OutcomeHandled + OutcomeRejected +) + +type ExecuteResult struct { + Outcome Outcome + Command string + Reply string + Err error +} + +type Executor struct { + reg *Registry +} + +func NewExecutor(reg *Registry) *Executor { + return &Executor{reg: reg} +} + +func (e *Executor) Execute(ctx context.Context, req Request, _ any) ExecuteResult { + cmdName, ok := parseCommandName(req.Text) + if !ok { + return ExecuteResult{Outcome: OutcomePassthrough} + } + + if e == nil || e.reg == nil { + return ExecuteResult{Outcome: OutcomePassthrough, Command: cmdName} + } + + for _, def := range e.reg.ForChannel(req.Channel) { + if !matchesCommand(def, cmdName) { + continue + } + if def.Handler == nil { + return ExecuteResult{Outcome: OutcomePassthrough, Command: def.Name} + } + err := def.Handler(ctx, req) + return ExecuteResult{Outcome: OutcomeHandled, Command: def.Name, Err: err} + } + + for _, def := range e.reg.defs { + if !matchesCommand(def, cmdName) { + continue + } + return ExecuteResult{ + Outcome: OutcomeRejected, + Command: def.Name, + Reply: fmt.Sprintf("Command /%s is not supported on %s.", def.Name, req.Channel), + } + } + + return ExecuteResult{Outcome: OutcomePassthrough, Command: cmdName} +} + +func matchesCommand(def Definition, cmdName string) bool { + if def.Name == cmdName { + return true + } + return contains(def.Aliases, cmdName) +} diff --git a/pkg/commands/executor_test.go b/pkg/commands/executor_test.go new file mode 100644 index 000000000..b0d690f3e --- /dev/null +++ b/pkg/commands/executor_test.go @@ -0,0 +1,49 @@ +package commands + +import ( + "context" + "testing" +) + +func TestExecutor_RegisteredButUnsupported_ReturnsRejected(t *testing.T) { + defs := []Definition{{Name: "show", Channels: []string{"telegram"}}} + ex := NewExecutor(NewRegistry(defs)) + + res := ex.Execute(context.Background(), Request{Channel: "whatsapp", Text: "/show"}, nil) + if res.Outcome != OutcomeRejected { + t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeRejected) + } +} + +func TestExecutor_UnknownSlashCommand_ReturnsPassthrough(t *testing.T) { + defs := []Definition{{Name: "show", Channels: []string{"telegram"}}} + ex := NewExecutor(NewRegistry(defs)) + + res := ex.Execute(context.Background(), Request{Channel: "telegram", Text: "/unknown"}, nil) + if res.Outcome != OutcomePassthrough { + t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomePassthrough) + } +} + +func TestExecutor_SupportedCommandWithHandler_ReturnsHandled(t *testing.T) { + called := false + defs := []Definition{ + { + Name: "help", + Channels: []string{"telegram"}, + Handler: func(context.Context, Request) error { + called = true + return nil + }, + }, + } + ex := NewExecutor(NewRegistry(defs)) + + res := ex.Execute(context.Background(), Request{Channel: "telegram", Text: "/help@my_bot"}, nil) + if res.Outcome != OutcomeHandled { + t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled) + } + if !called { + t.Fatalf("expected handler to be called") + } +}