358 lines
10 KiB
Go
358 lines
10 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package pipeline
|
|
|
|
import (
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// --- test helpers ---
|
|
|
|
type stubHandler struct {
|
|
name string
|
|
phase Phase
|
|
fn func(*Context) error
|
|
called bool
|
|
callSeq *[]string
|
|
}
|
|
|
|
func (h *stubHandler) Name() string { return h.name }
|
|
func (h *stubHandler) Phase() Phase { return h.phase }
|
|
func (h *stubHandler) Handle(ctx *Context) error {
|
|
h.called = true
|
|
if h.callSeq != nil {
|
|
*h.callSeq = append(*h.callSeq, h.name)
|
|
}
|
|
if h.fn != nil {
|
|
return h.fn(ctx)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func newStub(name string, phase Phase, fn func(*Context) error) *stubHandler {
|
|
return &stubHandler{name: name, phase: phase, fn: fn}
|
|
}
|
|
|
|
func newStubWithSeq(name string, phase Phase, seq *[]string) *stubHandler {
|
|
return &stubHandler{name: name, phase: phase, callSeq: seq}
|
|
}
|
|
|
|
// --- tests ---
|
|
|
|
func TestNewEngine(t *testing.T) {
|
|
e := NewEngine()
|
|
if e == nil {
|
|
t.Fatal("NewEngine returned nil")
|
|
}
|
|
if got := e.HandlerCount(); got != 0 {
|
|
t.Errorf("HandlerCount = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCommandPathFallbackLookupHandlesNilEngine(t *testing.T) {
|
|
var engine *Engine
|
|
engine.SetCommandPathFallbackLookup(func(string) (CommandPathFallback, bool) {
|
|
t.Fatal("nil engine installed or called fallback lookup")
|
|
return CommandPathFallback{}, false
|
|
})
|
|
if entry, ok := engine.lookupCommandPathFallback("chat +bad"); ok || !reflect.DeepEqual(entry, CommandPathFallback{}) {
|
|
t.Fatalf("nil engine lookup = %#v, %v", entry, ok)
|
|
}
|
|
|
|
engine = NewEngine()
|
|
if entry, ok := engine.lookupCommandPathFallback("chat +bad"); ok || !reflect.DeepEqual(entry, CommandPathFallback{}) {
|
|
t.Fatalf("unset engine lookup = %#v, %v", entry, ok)
|
|
}
|
|
}
|
|
|
|
func TestRegisterAndHandlerCount(t *testing.T) {
|
|
e := NewEngine()
|
|
e.Register(newStub("a", PreParse, nil))
|
|
e.Register(newStub("b", PreParse, nil))
|
|
e.Register(newStub("c", PostParse, nil))
|
|
|
|
if got := e.HandlerCount(); got != 3 {
|
|
t.Errorf("HandlerCount = %d, want 3", got)
|
|
}
|
|
if got := len(e.Handlers(PreParse)); got != 2 {
|
|
t.Errorf("PreParse handlers = %d, want 2", got)
|
|
}
|
|
if got := len(e.Handlers(PostParse)); got != 1 {
|
|
t.Errorf("PostParse handlers = %d, want 1", got)
|
|
}
|
|
if got := len(e.Handlers(PreRequest)); got != 0 {
|
|
t.Errorf("PreRequest handlers = %d, want 0", got)
|
|
}
|
|
}
|
|
|
|
func TestRegisterAll(t *testing.T) {
|
|
e := NewEngine()
|
|
e.RegisterAll(
|
|
newStub("a", PreParse, nil),
|
|
newStub("b", PostParse, nil),
|
|
newStub("c", PreRequest, nil),
|
|
)
|
|
if got := e.HandlerCount(); got != 3 {
|
|
t.Errorf("HandlerCount = %d, want 3", got)
|
|
}
|
|
}
|
|
|
|
func TestHasHandlers(t *testing.T) {
|
|
e := NewEngine()
|
|
if e.HasHandlers(PreParse) {
|
|
t.Error("HasHandlers(PreParse) = true on empty engine")
|
|
}
|
|
e.Register(newStub("a", PreParse, nil))
|
|
if !e.HasHandlers(PreParse) {
|
|
t.Error("HasHandlers(PreParse) = false after registration")
|
|
}
|
|
if e.HasHandlers(PostResponse) {
|
|
t.Error("HasHandlers(PostResponse) = true, want false")
|
|
}
|
|
}
|
|
|
|
func TestRunPhaseChainOrder(t *testing.T) {
|
|
var seq []string
|
|
e := NewEngine()
|
|
e.Register(newStubWithSeq("first", PreParse, &seq))
|
|
e.Register(newStubWithSeq("second", PreParse, &seq))
|
|
e.Register(newStubWithSeq("third", PreParse, &seq))
|
|
|
|
ctx := &Context{Args: []string{"test"}}
|
|
if err := e.RunPhase(PreParse, ctx); err != nil {
|
|
t.Fatalf("RunPhase returned error: %v", err)
|
|
}
|
|
want := "first,second,third"
|
|
got := strings.Join(seq, ",")
|
|
if got != want {
|
|
t.Errorf("execution order = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunPhaseContextMutationPropagates(t *testing.T) {
|
|
e := NewEngine()
|
|
e.Register(newStub("append-foo", PreParse, func(ctx *Context) error {
|
|
ctx.Args = append(ctx.Args, "--foo")
|
|
return nil
|
|
}))
|
|
e.Register(newStub("append-bar", PreParse, func(ctx *Context) error {
|
|
ctx.Args = append(ctx.Args, "--bar")
|
|
return nil
|
|
}))
|
|
|
|
ctx := &Context{Args: []string{"cmd"}}
|
|
if err := e.RunPhase(PreParse, ctx); err != nil {
|
|
t.Fatalf("RunPhase error: %v", err)
|
|
}
|
|
want := "cmd,--foo,--bar"
|
|
got := strings.Join(ctx.Args, ",")
|
|
if got != want {
|
|
t.Errorf("Args = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunPhaseErrorAbortsChain(t *testing.T) {
|
|
boom := errors.New("boom")
|
|
h1 := newStub("ok", PreParse, nil)
|
|
h2 := newStub("fail", PreParse, func(*Context) error { return boom })
|
|
h3 := newStub("never", PreParse, nil)
|
|
|
|
e := NewEngine()
|
|
e.RegisterAll(h1, h2, h3)
|
|
|
|
ctx := &Context{}
|
|
err := e.RunPhase(PreParse, ctx)
|
|
if err == nil {
|
|
t.Fatal("expected error, got nil")
|
|
}
|
|
if !errors.Is(err, boom) {
|
|
t.Errorf("error = %v, want wrapped boom", err)
|
|
}
|
|
if !strings.Contains(err.Error(), "fail") {
|
|
t.Errorf("error should contain handler name, got %q", err.Error())
|
|
}
|
|
var handlerErr *HandlerError
|
|
if !errors.As(err, &handlerErr) || handlerErr.Phase != PreParse || handlerErr.Handler != "fail" || handlerErr.Unwrap() != boom {
|
|
t.Fatalf("handler error = %#v, want pre-parse/fail wrapping boom", handlerErr)
|
|
}
|
|
if !h1.called {
|
|
t.Error("h1 should have been called")
|
|
}
|
|
if !h2.called {
|
|
t.Error("h2 should have been called")
|
|
}
|
|
if h3.called {
|
|
t.Error("h3 should NOT have been called after error")
|
|
}
|
|
}
|
|
|
|
func TestRunPhaseEmptyPhaseNoError(t *testing.T) {
|
|
e := NewEngine()
|
|
ctx := &Context{}
|
|
if err := e.RunPhase(PreParse, ctx); err != nil {
|
|
t.Errorf("RunPhase on empty phase returned error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRunAllPhasesOrder(t *testing.T) {
|
|
var seq []string
|
|
e := NewEngine()
|
|
e.Register(newStubWithSeq("post-resp", PostResponse, &seq))
|
|
e.Register(newStubWithSeq("pre-parse", PreParse, &seq))
|
|
e.Register(newStubWithSeq("register", Register, &seq))
|
|
e.Register(newStubWithSeq("post-parse", PostParse, &seq))
|
|
e.Register(newStubWithSeq("pre-req", PreRequest, &seq))
|
|
|
|
ctx := &Context{}
|
|
if err := e.Run(ctx); err != nil {
|
|
t.Fatalf("Run returned error: %v", err)
|
|
}
|
|
want := "register,pre-parse,post-parse,pre-req,post-resp"
|
|
got := strings.Join(seq, ",")
|
|
if got != want {
|
|
t.Errorf("phase execution order = %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestRunAbortsOnPhaseError(t *testing.T) {
|
|
var seq []string
|
|
e := NewEngine()
|
|
e.Register(newStubWithSeq("pre-parse", PreParse, &seq))
|
|
e.Register(newStub("post-parse-fail", PostParse, func(*Context) error {
|
|
return errors.New("fail")
|
|
}))
|
|
e.Register(newStubWithSeq("pre-req", PreRequest, &seq))
|
|
|
|
ctx := &Context{}
|
|
err := e.Run(ctx)
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
if len(seq) != 1 || seq[0] != "pre-parse" {
|
|
t.Errorf("seq = %v, want [pre-parse]", seq)
|
|
}
|
|
}
|
|
|
|
func TestContextAddCorrection(t *testing.T) {
|
|
ctx := &Context{}
|
|
ctx.AddCorrection("alias", PreParse, "--userId", "--userId", "--user-id", "alias")
|
|
ctx.AddCorrection("sticky", PreParse, "--limit", "--limit100", "--limit 100", "sticky")
|
|
|
|
if got := len(ctx.Corrections); got != 2 {
|
|
t.Fatalf("Corrections count = %d, want 2", got)
|
|
}
|
|
c := ctx.Corrections[0]
|
|
if c.Handler != "alias" || c.Original != "--userId" || c.Corrected != "--user-id" {
|
|
t.Errorf("first correction = %+v", c)
|
|
}
|
|
}
|
|
|
|
func TestContextFlagProtectionAndConflictError(t *testing.T) {
|
|
var nilContext *Context
|
|
nilContext.ProtectFlag("uid", FlagProtectionBlocked)
|
|
if nilContext.IsFlagProtected("uid") {
|
|
t.Fatal("nil context reported a protected flag")
|
|
}
|
|
|
|
ctx := &Context{}
|
|
ctx.ProtectFlag("", FlagProtectionBlocked)
|
|
if ctx.ProtectedFlags != nil {
|
|
t.Fatalf("empty flag initialized protection map: %#v", ctx.ProtectedFlags)
|
|
}
|
|
ctx.ProtectFlag("uid", FlagProtectionAmbiguous)
|
|
if !ctx.IsFlagProtected("uid") || ctx.IsFlagProtected("missing") {
|
|
t.Fatalf("protection lookup mismatch: %#v", ctx.ProtectedFlags)
|
|
}
|
|
|
|
err := (&FlagConflictError{
|
|
Command: "dws demo run",
|
|
Canonical: "user",
|
|
Spellings: []string{"--user-id", "uid"},
|
|
}).Error()
|
|
if !strings.Contains(err, `for --user on "dws demo run": --user-id, --uid`) {
|
|
t.Fatalf("FlagConflictError.Error() = %q", err)
|
|
}
|
|
|
|
boolErr := (&BoolValueConflictError{
|
|
Command: "dws demo run",
|
|
Flag: "--yes",
|
|
Values: []string{"true", "false"},
|
|
}).Error()
|
|
if !strings.Contains(boolErr, `for --yes on "dws demo run": false, true`) {
|
|
t.Fatalf("BoolValueConflictError.Error() = %q", boolErr)
|
|
}
|
|
}
|
|
|
|
func TestPhaseString(t *testing.T) {
|
|
tests := []struct {
|
|
phase Phase
|
|
want string
|
|
}{
|
|
{Register, "register"},
|
|
{PreParse, "pre-parse"},
|
|
{PostParse, "post-parse"},
|
|
{PreRequest, "pre-request"},
|
|
{PostResponse, "post-response"},
|
|
{Phase(99), "unknown"},
|
|
}
|
|
for _, tt := range tests {
|
|
if got := tt.phase.String(); got != tt.want {
|
|
t.Errorf("Phase(%d).String() = %q, want %q", tt.phase, got, tt.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRunPhaseErrorContainsPhaseAndHandler(t *testing.T) {
|
|
e := NewEngine()
|
|
e.Register(newStub("my-handler", PostParse, func(*Context) error {
|
|
return errors.New("bad value")
|
|
}))
|
|
|
|
err := e.RunPhase(PostParse, &Context{})
|
|
if err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
msg := err.Error()
|
|
if !strings.Contains(msg, "post-parse") {
|
|
t.Errorf("error should contain phase name, got %q", msg)
|
|
}
|
|
if !strings.Contains(msg, "my-handler") {
|
|
t.Errorf("error should contain handler name, got %q", msg)
|
|
}
|
|
}
|
|
|
|
func TestMultipleHandlersSameAndDifferentPhases(t *testing.T) {
|
|
var seq []string
|
|
e := NewEngine()
|
|
e.Register(newStubWithSeq("pp1", PreParse, &seq))
|
|
e.Register(newStubWithSeq("pp2", PreParse, &seq))
|
|
e.Register(newStubWithSeq("op1", PostParse, &seq))
|
|
e.Register(newStubWithSeq("rq1", PreRequest, &seq))
|
|
e.Register(newStubWithSeq("rq2", PreRequest, &seq))
|
|
e.Register(newStubWithSeq("rs1", PostResponse, &seq))
|
|
|
|
ctx := &Context{}
|
|
if err := e.Run(ctx); err != nil {
|
|
t.Fatalf("Run error: %v", err)
|
|
}
|
|
want := "pp1,pp2,op1,rq1,rq2,rs1"
|
|
got := strings.Join(seq, ",")
|
|
if got != want {
|
|
t.Errorf("order = %q, want %q", got, want)
|
|
}
|
|
}
|