mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-11 08:44:00 +08:00
- Implemented command-line flag registration and execution for plugins with priority-based conflict resolution. - Enabled plugin-owned command-line flag execution and persistence of plugin-auth data. - Added new `Host` methods to support command-line capabilities, including flag normalization, validation, and execution state management. - Introduced unit tests to ensure coverage for command-line plugin functionality, including auth data persistence. - Updated configs to normalize plugins during initialization.
213 lines
7.2 KiB
Go
213 lines
7.2 KiB
Go
package pluginhost
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"flag"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
sdkAuth "github.com/router-for-me/CLIProxyAPI/v7/sdk/auth"
|
|
coreauth "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"
|
|
)
|
|
|
|
func TestRegisterCommandLineFlagsSkipsNativeAndUsesPriority(t *testing.T) {
|
|
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
|
|
flagSet.SetOutput(&bytes.Buffer{})
|
|
flagSet.Bool("native", false, "native flag")
|
|
|
|
high := &commandLinePluginDouble{
|
|
flags: []pluginapi.CommandLineFlag{
|
|
{Name: "native", Type: "bool", Usage: "conflicting native flag"},
|
|
{Name: "help", Type: "bool", Usage: "reserved help flag"},
|
|
{Name: "h", Type: "bool", Usage: "reserved short help flag"},
|
|
{Name: "shared", Type: "string", Usage: "shared flag"},
|
|
},
|
|
}
|
|
low := &commandLinePluginDouble{
|
|
flags: []pluginapi.CommandLineFlag{
|
|
{Name: "shared", Type: "string", Usage: "lower priority shared flag"},
|
|
{Name: "low-only", Type: "int", Usage: "low priority flag"},
|
|
},
|
|
}
|
|
host := newHostWithRecords(
|
|
capabilityRecord{id: "low", priority: 1, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{CommandLinePlugin: low}}},
|
|
capabilityRecord{id: "high", priority: 10, plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{CommandLinePlugin: high}}},
|
|
)
|
|
|
|
host.RegisterCommandLineFlags(context.Background(), flagSet)
|
|
|
|
if flagSet.Lookup("native") == nil {
|
|
t.Fatal("native flag missing")
|
|
}
|
|
if flagSet.Lookup("shared") == nil {
|
|
t.Fatal("shared plugin flag missing")
|
|
}
|
|
if flagSet.Lookup("low-only") == nil {
|
|
t.Fatal("low-only plugin flag missing")
|
|
}
|
|
if got := host.commandLineFlags["shared"].pluginID; got != "high" {
|
|
t.Fatalf("shared owner = %q, want high", got)
|
|
}
|
|
if _, exists := host.commandLineFlags["native"]; exists {
|
|
t.Fatal("native flag was claimed by plugin")
|
|
}
|
|
if _, exists := host.commandLineFlags["help"]; exists {
|
|
t.Fatal("reserved help flag was claimed by plugin")
|
|
}
|
|
if _, exists := host.commandLineFlags["h"]; exists {
|
|
t.Fatal("reserved h flag was claimed by plugin")
|
|
}
|
|
}
|
|
|
|
func TestExecuteCommandLinePassesAllArgsAndTriggeredFlags(t *testing.T) {
|
|
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
|
|
flagSet.SetOutput(&bytes.Buffer{})
|
|
plugin := &commandLinePluginDouble{
|
|
flags: []pluginapi.CommandLineFlag{{
|
|
Name: "plugin-command",
|
|
Type: "bool",
|
|
}},
|
|
}
|
|
host := newHostWithRecords(capabilityRecord{
|
|
id: "alpha",
|
|
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{CommandLinePlugin: plugin}},
|
|
})
|
|
host.runtimeConfig = &config.Config{AuthDir: "/tmp/plugin-auth"}
|
|
host.RegisterCommandLineFlags(context.Background(), flagSet)
|
|
|
|
if errParse := flagSet.Parse([]string{"-plugin-command", "tail"}); errParse != nil {
|
|
t.Fatalf("Parse() error = %v", errParse)
|
|
}
|
|
if !host.HasTriggeredCommandLineFlags() {
|
|
t.Fatal("HasTriggeredCommandLineFlags() = false, want true")
|
|
}
|
|
|
|
exitCode, handled := host.ExecuteCommandLine(context.Background(), "cliproxy", []string{"-plugin-command", "tail"}, "/tmp/config.yaml", flagSet)
|
|
if !handled {
|
|
t.Fatal("ExecuteCommandLine() handled = false, want true")
|
|
}
|
|
if exitCode != 0 {
|
|
t.Fatalf("ExecuteCommandLine() exitCode = %d, want 0", exitCode)
|
|
}
|
|
if len(plugin.execRequests) != 1 {
|
|
t.Fatalf("execute calls = %d, want 1", len(plugin.execRequests))
|
|
}
|
|
req := plugin.execRequests[0]
|
|
if req.Program != "cliproxy" || req.ConfigPath != "/tmp/config.yaml" {
|
|
t.Fatalf("execution request = %#v, want program and config path", req)
|
|
}
|
|
if req.Host.AuthDir != "/tmp/plugin-auth" {
|
|
t.Fatalf("execution request host = %#v, want auth dir", req.Host)
|
|
}
|
|
if len(req.Args) != 2 || req.Args[0] != "-plugin-command" || req.Args[1] != "tail" {
|
|
t.Fatalf("Args = %#v, want full args", req.Args)
|
|
}
|
|
if got := req.TriggeredFlags["plugin-command"]; !got.Set || got.Value != "true" {
|
|
t.Fatalf("TriggeredFlags[plugin-command] = %#v, want set true", got)
|
|
}
|
|
}
|
|
|
|
func TestExecuteCommandLinePersistsReturnedAuths(t *testing.T) {
|
|
authDir := t.TempDir()
|
|
store := &commandLineAuthStore{}
|
|
origStore := sdkAuth.GetTokenStore()
|
|
sdkAuth.RegisterTokenStore(store)
|
|
defer sdkAuth.RegisterTokenStore(origStore)
|
|
|
|
flagSet := flag.NewFlagSet("test", flag.ContinueOnError)
|
|
flagSet.SetOutput(&bytes.Buffer{})
|
|
plugin := &commandLinePluginDouble{
|
|
flags: []pluginapi.CommandLineFlag{{
|
|
Name: "plugin-login",
|
|
Type: "bool",
|
|
}},
|
|
response: pluginapi.CommandLineExecutionResponse{
|
|
Stdout: []byte("login ok\n"),
|
|
Auths: []pluginapi.AuthData{{
|
|
Provider: "Qoder",
|
|
ID: "qoder.json",
|
|
FileName: "qoder.json",
|
|
Label: "Luis",
|
|
StorageJSON: []byte(`{"token":"secret"}`),
|
|
}},
|
|
},
|
|
}
|
|
host := newHostWithRecords(capabilityRecord{
|
|
id: "qoder",
|
|
plugin: pluginapi.Plugin{Capabilities: pluginapi.Capabilities{CommandLinePlugin: plugin}},
|
|
})
|
|
host.runtimeConfig = &config.Config{AuthDir: authDir}
|
|
host.RegisterCommandLineFlags(context.Background(), flagSet)
|
|
|
|
if errParse := flagSet.Parse([]string{"-plugin-login"}); errParse != nil {
|
|
t.Fatalf("Parse() error = %v", errParse)
|
|
}
|
|
|
|
exitCode, handled := host.ExecuteCommandLine(context.Background(), "cliproxy", []string{"-plugin-login"}, "/tmp/config.yaml", flagSet)
|
|
if !handled {
|
|
t.Fatal("ExecuteCommandLine() handled = false, want true")
|
|
}
|
|
if exitCode != 0 {
|
|
t.Fatalf("ExecuteCommandLine() exitCode = %d, want 0", exitCode)
|
|
}
|
|
if store.baseDir != authDir {
|
|
t.Fatalf("store baseDir = %q, want %q", store.baseDir, authDir)
|
|
}
|
|
if len(store.saved) != 1 {
|
|
t.Fatalf("saved auths = %d, want 1", len(store.saved))
|
|
}
|
|
saved := store.saved[0]
|
|
if saved.Provider != "qoder" || saved.ID != "qoder.json" || saved.FileName != "qoder.json" {
|
|
t.Fatalf("saved auth = %#v, want normalized qoder auth", saved)
|
|
}
|
|
if saved.Storage == nil {
|
|
t.Fatal("saved auth storage = nil, want plugin token storage")
|
|
}
|
|
if store.paths[0] != filepath.Join(authDir, "qoder.json") {
|
|
t.Fatalf("saved path = %q, want auth dir path", store.paths[0])
|
|
}
|
|
}
|
|
|
|
type commandLinePluginDouble struct {
|
|
flags []pluginapi.CommandLineFlag
|
|
execRequests []pluginapi.CommandLineExecutionRequest
|
|
response pluginapi.CommandLineExecutionResponse
|
|
}
|
|
|
|
func (p *commandLinePluginDouble) RegisterCommandLine(context.Context, pluginapi.CommandLineRegistrationRequest) (pluginapi.CommandLineRegistrationResponse, error) {
|
|
return pluginapi.CommandLineRegistrationResponse{Flags: p.flags}, nil
|
|
}
|
|
|
|
func (p *commandLinePluginDouble) ExecuteCommandLine(ctx context.Context, req pluginapi.CommandLineExecutionRequest) (pluginapi.CommandLineExecutionResponse, error) {
|
|
p.execRequests = append(p.execRequests, req)
|
|
return p.response, nil
|
|
}
|
|
|
|
type commandLineAuthStore struct {
|
|
baseDir string
|
|
saved []*coreauth.Auth
|
|
paths []string
|
|
}
|
|
|
|
func (s *commandLineAuthStore) List(context.Context) ([]*coreauth.Auth, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (s *commandLineAuthStore) Save(_ context.Context, auth *coreauth.Auth) (string, error) {
|
|
s.saved = append(s.saved, auth.Clone())
|
|
path := filepath.Join(s.baseDir, auth.FileName)
|
|
s.paths = append(s.paths, path)
|
|
return path, nil
|
|
}
|
|
|
|
func (s *commandLineAuthStore) Delete(context.Context, string) error {
|
|
return nil
|
|
}
|
|
|
|
func (s *commandLineAuthStore) SetBaseDir(dir string) {
|
|
s.baseDir = dir
|
|
}
|