diff --git a/pkg/commands/executor.go b/pkg/commands/executor.go index 29a62e08c..50e84a2fb 100644 --- a/pkg/commands/executor.go +++ b/pkg/commands/executor.go @@ -28,7 +28,7 @@ func NewExecutor(reg *Registry) *Executor { return &Executor{reg: reg} } -func (e *Executor) Execute(ctx context.Context, req Request, _ any) ExecuteResult { +func (e *Executor) Execute(ctx context.Context, req Request) ExecuteResult { cmdName, ok := parseCommandName(req.Text) if !ok { return ExecuteResult{Outcome: OutcomePassthrough} @@ -38,16 +38,26 @@ func (e *Executor) Execute(ctx context.Context, req Request, _ any) ExecuteResul return ExecuteResult{Outcome: OutcomePassthrough, Command: cmdName} } + matchedSupported := false + passthroughCommand := "" + for _, def := range e.reg.ForChannel(req.Channel) { if !matchesCommand(def, cmdName) { continue } + matchedSupported = true + if passthroughCommand == "" { + passthroughCommand = def.Name + } if def.Handler == nil { - return ExecuteResult{Outcome: OutcomePassthrough, Command: def.Name} + continue } err := def.Handler(ctx, req) return ExecuteResult{Outcome: OutcomeHandled, Command: def.Name, Err: err} } + if matchedSupported { + return ExecuteResult{Outcome: OutcomePassthrough, Command: passthroughCommand} + } for _, def := range e.reg.defs { if !matchesCommand(def, cmdName) { diff --git a/pkg/commands/executor_test.go b/pkg/commands/executor_test.go index b0d690f3e..130f32512 100644 --- a/pkg/commands/executor_test.go +++ b/pkg/commands/executor_test.go @@ -2,6 +2,7 @@ package commands import ( "context" + "errors" "testing" ) @@ -9,7 +10,7 @@ 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) + res := ex.Execute(context.Background(), Request{Channel: "whatsapp", Text: "/show"}) if res.Outcome != OutcomeRejected { t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeRejected) } @@ -19,7 +20,7 @@ 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) + res := ex.Execute(context.Background(), Request{Channel: "telegram", Text: "/unknown"}) if res.Outcome != OutcomePassthrough { t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomePassthrough) } @@ -39,7 +40,7 @@ func TestExecutor_SupportedCommandWithHandler_ReturnsHandled(t *testing.T) { } ex := NewExecutor(NewRegistry(defs)) - res := ex.Execute(context.Background(), Request{Channel: "telegram", Text: "/help@my_bot"}, nil) + res := ex.Execute(context.Background(), Request{Channel: "telegram", Text: "/help@my_bot"}) if res.Outcome != OutcomeHandled { t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled) } @@ -47,3 +48,113 @@ func TestExecutor_SupportedCommandWithHandler_ReturnsHandled(t *testing.T) { t.Fatalf("expected handler to be called") } } + +func TestExecutor_AliasUnsupportedChannel_ReturnsRejected(t *testing.T) { + defs := []Definition{ + { + Name: "show", + Aliases: []string{"display"}, + Channels: []string{"telegram"}, + }, + } + ex := NewExecutor(NewRegistry(defs)) + + res := ex.Execute(context.Background(), Request{Channel: "whatsapp", Text: "/display"}) + if res.Outcome != OutcomeRejected { + t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeRejected) + } + if res.Command != "show" { + t.Fatalf("command=%q, want=%q", res.Command, "show") + } +} + +func TestExecutor_AliasWithHandler_ReturnsHandled(t *testing.T) { + called := false + defs := []Definition{ + { + Name: "new", + Aliases: []string{"reset"}, + 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: "/reset"}) + if res.Outcome != OutcomeHandled { + t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled) + } + if res.Command != "new" { + t.Fatalf("command=%q, want=%q", res.Command, "new") + } + if !called { + t.Fatalf("expected handler to be called") + } +} + +func TestExecutor_SupportedCommandWithNilHandler_ReturnsPassthrough(t *testing.T) { + defs := []Definition{ + {Name: "session", Channels: []string{"telegram"}}, + } + ex := NewExecutor(NewRegistry(defs)) + + res := ex.Execute(context.Background(), Request{Channel: "telegram", Text: "/session list"}) + if res.Outcome != OutcomePassthrough { + t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomePassthrough) + } + if res.Command != "session" { + t.Fatalf("command=%q, want=%q", res.Command, "session") + } +} + +func TestExecutor_NilHandlerDoesNotMaskLaterHandler(t *testing.T) { + called := false + defs := []Definition{ + {Name: "session", Channels: []string{"telegram"}}, + { + Name: "session", + 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: "/session"}) + if res.Outcome != OutcomeHandled { + t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled) + } + if res.Command != "session" { + t.Fatalf("command=%q, want=%q", res.Command, "session") + } + if !called { + t.Fatalf("expected later handler to be called") + } +} + +func TestExecutor_HandlerErrorIsPropagated(t *testing.T) { + wantErr := errors.New("handler failed") + defs := []Definition{ + { + Name: "help", + Channels: []string{"telegram"}, + Handler: func(context.Context, Request) error { + return wantErr + }, + }, + } + ex := NewExecutor(NewRegistry(defs)) + + res := ex.Execute(context.Background(), Request{Channel: "telegram", Text: "/help"}) + if res.Outcome != OutcomeHandled { + t.Fatalf("outcome=%v, want=%v", res.Outcome, OutcomeHandled) + } + if !errors.Is(res.Err, wantErr) { + t.Fatalf("err=%v, want=%v", res.Err, wantErr) + } +}