package tool import ( "context" "errors" "reflect" "testing" "time" "github.com/kami/maven/internal/ipc" ) // fakeAPI — an in-memory tool store for the executor/matcher tests. type fakeAPI struct{ tools map[string]ipc.Tool } func (f fakeAPI) LookupTool(_ context.Context, name string) (ipc.Tool, error) { t, ok := f.tools[name] if !ok { return ipc.Tool{}, ipc.ErrToolNotFound } return t, nil } func (f fakeAPI) ListTools(_ context.Context, status string) ([]ipc.Tool, error) { var out []ipc.Tool for _, t := range f.tools { if status == "" || t.Status == status { out = append(out, t) } } return out, nil } func (f fakeAPI) ProposeTool(_ context.Context, _, _, _ string, _ time.Time) (bool, error) { return true, nil } // TestExec covers the allowlist boundary: enabled runs, unknown/proposed refuse, // destructive needs confirm, and args land as argv (no shell) after the prefix. func TestExec(t *testing.T) { api := fakeAPI{tools: map[string]ipc.Tool{ "restart": {Name: "restart", Scope: "homelab", Cmd: []string{"systemctl", "restart"}, Status: "enabled"}, "drop": {Name: "drop", Scope: "homelab", Cmd: []string{"dropdb"}, Destructive: true, Status: "enabled"}, "draft": {Name: "draft", Scope: "homelab", Cmd: []string{"x"}, Status: "proposed"}, }} var gotArgv []string e := NewExecutor(api, 0) e.run = func(_ context.Context, argv []string) (string, error) { gotArgv = argv; return "ok", nil } // enabled → runs, args appended to the fixed prefix as argv. out, err := e.Exec(context.Background(), "restart", []string{"nginx; rm -rf /"}, false) if err != nil || out != "ok" { t.Fatalf("enabled: out=%q err=%v", out, err) } want := []string{"systemctl", "restart", "nginx; rm -rf /"} if !reflect.DeepEqual(gotArgv, want) { t.Fatalf("argv=%v want %v (injection must stay one argv element)", gotArgv, want) } // unknown → refuse. if _, err := e.Exec(context.Background(), "nope", nil, false); !errors.Is(err, ErrNotEnabled) { t.Fatalf("unknown: err=%v want ErrNotEnabled", err) } // proposed (not enabled) → refuse. if _, err := e.Exec(context.Background(), "draft", nil, false); !errors.Is(err, ErrNotEnabled) { t.Fatalf("proposed: err=%v want ErrNotEnabled", err) } // destructive unconfirmed → needs confirm; confirmed → runs. if _, err := e.Exec(context.Background(), "drop", nil, false); !errors.Is(err, ErrNeedsConfirm) { t.Fatalf("destructive: err=%v want ErrNeedsConfirm", err) } if _, err := e.Exec(context.Background(), "drop", []string{"db"}, true); err != nil { t.Fatalf("destructive confirmed: err=%v", err) } if want := []string{"dropdb", "db"}; !reflect.DeepEqual(gotArgv, want) { t.Fatalf("confirmed argv=%v want %v", gotArgv, want) } // matcher allowlist = enabled names only (proposed excluded). m := NewMatcher(api) fn, args, ok := m.Match("restart nginx") if !ok || fn != "restart" || !reflect.DeepEqual(args, []string{"nginx"}) { t.Fatalf("match: fn=%q args=%v ok=%v", fn, args, ok) } if _, _, ok := m.Match("draft something"); ok { t.Fatal("proposed tool must not match (not enabled)") } }