2305 lines · 73.0 KB · Go
RawDownload
package pluginhost import (	"bytes"	"context"	"encoding/json"	"fmt"	"net/http"	"path/filepath"	"slices"	"strings"	"sync"	"sync/atomic"	"testing"	"time" 	"github.com/router-for-me/CLIProxyAPI/v7/internal/config"	"github.com/router-for-me/CLIProxyAPI/v7/internal/thinking"	"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi"	"github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginapi"	log "github.com/sirupsen/logrus"	"github.com/tidwall/gjson") func enabledPluginConfigs(ids ...string) map[string]config.PluginInstanceConfig {	enabled := true	configs := make(map[string]config.PluginInstanceConfig, len(ids))	for _, id := range ids {		configs[id] = config.PluginInstanceConfig{Enabled: &enabled}	}	return configs} func TestHostApplyConfig_DisabledGlobalSkipsSnapshot(t *testing.T) {	loader := newTestSymbolLoader()	h := NewForTest(loader) 	h.ApplyConfig(context.Background(), &config.Config{		Plugins: config.PluginsConfig{			Enabled: false,			Dir:     makePluginDir(t, "alpha"),		},	}) 	if loader.openCalls != 0 {		t.Fatalf("Open calls = %d, want 0", loader.openCalls)	}	snap := h.Snapshot()	if snap.enabled || len(snap.records) != 0 {		t.Fatalf("Snapshot() = %+v, want empty disabled snapshot", snap)	}} func TestHostApplyConfig_DisabledGlobalDoesNotResolvePluginsDir(t *testing.T) {	loader := newTestSymbolLoader()	plugin := &testPlugin{		registerResult:    validTestPlugin("alpha"),		reconfigureResult: validTestPlugin("alpha"),	}	loader.lookups["alpha"] = newTestSymbolLookup(plugin)	h := NewForTest(loader)	t.Cleanup(h.ShutdownAll) 	h.ApplyConfig(context.Background(), &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     makePluginDir(t, "alpha"),			Configs: enabledPluginConfigs("alpha"),		},	})	if !h.PluginRegistered("alpha") {		t.Fatal("PluginRegistered(alpha) = false, want true before disable")	} 	t.Setenv("HOME", "")	t.Setenv("USERPROFILE", "")	disabledCfg, errParseConfig := config.ParseConfigBytes([]byte(`plugins:  enabled: false  dir: "~/.cli-proxy-api/plugins"`))	if errParseConfig != nil {		t.Fatalf("ParseConfigBytes() error = %v", errParseConfig)	}	h.ApplyConfig(context.Background(), disabledCfg) 	if h.PluginRegistered("alpha") {		t.Fatal("PluginRegistered(alpha) = true, want false after disable")	}	if snap := h.Snapshot(); snap.enabled || len(snap.records) != 0 {		t.Fatalf("Snapshot() = %+v, want empty disabled snapshot", snap)	}} func TestHostApplyConfig_ExpandsPluginsDirLeadingTilde(t *testing.T) {	loader := newTestSymbolLoader()	plugin := &testPlugin{		registerResult:    validTestPlugin("alpha"),		reconfigureResult: validTestPlugin("alpha"),	}	loader.lookups["alpha"] = newTestSymbolLookup(plugin)	h := NewForTest(loader)	t.Cleanup(h.ShutdownAll) 	pluginsDir := makePluginDir(t, "alpha")	homeDir := filepath.Dir(pluginsDir)	t.Setenv("HOME", homeDir)	t.Setenv("USERPROFILE", homeDir)	h.ApplyConfig(context.Background(), &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     "~/" + filepath.ToSlash(filepath.Base(pluginsDir)),			Configs: enabledPluginConfigs("alpha"),		},	}) 	if loader.openCalls != 1 {		t.Fatalf("Open calls = %d, want 1", loader.openCalls)	}	if !h.PluginRegistered("alpha") {		t.Fatal("PluginRegistered(alpha) = false, want true")	}} func TestHostApplyConfig_DisabledPluginSkipsCapability(t *testing.T) {	enabled := false	loader := newTestSymbolLoader()	plugin := &testPlugin{		registerResult:    validTestPlugin("alpha"),		reconfigureResult: validTestPlugin("alpha"),	}	loader.lookups["alpha"] = newTestSymbolLookup(plugin)	h := NewForTest(loader) 	h.ApplyConfig(context.Background(), &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     makePluginDir(t, "alpha"),			Configs: map[string]config.PluginInstanceConfig{				"alpha": {Enabled: &enabled},			},		},	}) 	if plugin.registerCalls != 0 || plugin.reconfigureCalls != 0 {		t.Fatalf("calls = register %d reconfigure %d, want 0", plugin.registerCalls, plugin.reconfigureCalls)	}	if loader.openCalls != 0 {		t.Fatalf("Open calls = %d, want 0", loader.openCalls)	}	if len(h.activeRecords()) != 0 {		t.Fatalf("Snapshot records = %d, want 0", len(h.activeRecords()))	}} func TestHostApplyConfig_DefaultDisabledPluginSkipsLoad(t *testing.T) {	loader := newTestSymbolLoader()	plugin := &testPlugin{		registerResult:    validTestPlugin("alpha"),		reconfigureResult: validTestPlugin("alpha"),	}	loader.lookups["alpha"] = newTestSymbolLookup(plugin)	h := NewForTest(loader) 	h.ApplyConfig(context.Background(), &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     makePluginDir(t, "alpha"),		},	}) 	if plugin.registerCalls != 0 || loader.openCalls != 0 {		t.Fatalf("calls = register %d open %d, want 0", plugin.registerCalls, loader.openCalls)	}	if len(h.activeRecords()) != 0 {		t.Fatalf("Snapshot records = %d, want 0", len(h.activeRecords()))	}} func TestPluginLoadedTracksLoadedPluginAfterDisabled(t *testing.T) {	disabled := false	loader := newTestSymbolLoader()	plugin := &testPlugin{		registerResult:    validTestPlugin("alpha"),		reconfigureResult: validTestPlugin("alpha"),	}	loader.lookups["alpha"] = newTestSymbolLookup(plugin)	h := NewForTest(loader)	t.Cleanup(h.ShutdownAll)	pluginsDir := makePluginDir(t, "alpha") 	h.ApplyConfig(context.Background(), &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     pluginsDir,			Configs: enabledPluginConfigs("alpha"),		},	}) 	if !h.PluginLoaded("alpha") {		t.Fatal("PluginLoaded(alpha) = false, want true after load")	}	if !h.PluginRegistered("alpha") {		t.Fatal("PluginRegistered(alpha) = false, want true after load")	}	if len(h.RegisteredPlugins()) != 1 {		t.Fatalf("RegisteredPlugins() len = %d, want 1", len(h.RegisteredPlugins()))	} 	h.ApplyConfig(context.Background(), &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     pluginsDir,			Configs: map[string]config.PluginInstanceConfig{				"alpha": {Enabled: &disabled},			},		},	}) 	if len(h.RegisteredPlugins()) != 0 {		t.Fatalf("RegisteredPlugins() len = %d, want 0 after disable", len(h.RegisteredPlugins()))	}	if h.PluginRegistered("alpha") {		t.Fatal("PluginRegistered(alpha) = true, want false after disable")	}	if !h.PluginLoaded("alpha") {		t.Fatal("PluginLoaded(alpha) = false, want true while library remains loaded")	} 	h.ShutdownAll()	if h.PluginLoaded("alpha") {		t.Fatal("PluginLoaded(alpha) = true, want false after ShutdownAll")	}} func TestHostUnloadPluginTargetsOnlyRequestedPlugin(t *testing.T) {	loader := newTestSymbolLoader()	alpha := &testPlugin{		registerResult:    validTestPlugin("alpha"),		reconfigureResult: validTestPlugin("alpha"),	}	bravo := &testPlugin{		registerResult:    validTestPlugin("bravo"),		reconfigureResult: validTestPlugin("bravo"),	}	alphaLookup := newTestSymbolLookup(alpha)	bravoLookup := newTestSymbolLookup(bravo)	loader.lookups["alpha"] = alphaLookup	loader.lookups["bravo"] = bravoLookup	h := NewForTest(loader)	t.Cleanup(h.ShutdownAll)	cfg := &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     makePluginDir(t, "alpha", "bravo"),			Configs: enabledPluginConfigs("alpha", "bravo"),		},	} 	h.ApplyConfig(context.Background(), cfg) 	if !h.UnloadPlugin("alpha") {		t.Fatal("UnloadPlugin(alpha) = false, want true")	}	if h.PluginLoaded("alpha") {		t.Fatal("PluginLoaded(alpha) = true, want false after targeted unload")	}	if !h.PluginLoaded("bravo") {		t.Fatal("PluginLoaded(bravo) = false, want true after alpha unload")	}	if alphaLookup.shutdownCalls != 1 {		t.Fatalf("alpha shutdown calls = %d, want 1", alphaLookup.shutdownCalls)	}	if bravoLookup.shutdownCalls != 0 {		t.Fatalf("bravo shutdown calls = %d, want 0", bravoLookup.shutdownCalls)	}	plugins := h.RegisteredPlugins()	if len(plugins) != 1 || plugins[0].ID != "bravo" {		t.Fatalf("RegisteredPlugins() = %#v, want only bravo", plugins)	} 	h.ApplyConfig(context.Background(), cfg) 	if loader.openCalls != 3 {		t.Fatalf("Open calls = %d, want 3", loader.openCalls)	}	if alpha.registerCalls != 2 {		t.Fatalf("alpha register calls = %d, want 2", alpha.registerCalls)	}	if bravo.registerCalls != 1 {		t.Fatalf("bravo register calls = %d, want 1", bravo.registerCalls)	}	if bravo.reconfigureCalls != 1 {		t.Fatalf("bravo reconfigure calls = %d, want 1", bravo.reconfigureCalls)	}} func TestHostApplyConfigRegistersPluginThinkingApplier(t *testing.T) {	loader := newTestSymbolLoader()	plugin := &testPlugin{		registerResult:    validTestPlugin("alpha"),		reconfigureResult: validTestPlugin("alpha"),	}	plugin.registerResult.Capabilities.ThinkingApplier = testThinkingCapability{provider: "plugin-thinking"}	plugin.reconfigureResult.Capabilities.ThinkingApplier = testThinkingCapability{provider: "plugin-thinking"}	loader.lookups["alpha"] = newTestSymbolLookup(plugin)	h := NewForTest(loader)	cfg := &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     makePluginDir(t, "alpha"),			Configs: enabledPluginConfigs("alpha"),		},	}	t.Cleanup(func() {		h.ApplyConfig(context.Background(), &config.Config{			Plugins: config.PluginsConfig{				Enabled: false,				Dir:     cfg.Plugins.Dir,			},		})	}) 	h.ApplyConfig(context.Background(), cfg) 	out, errApply := thinking.ApplyThinking([]byte(`{"model":"plugin-model"}`), "plugin-model(10240)", "openai", "plugin-thinking", "plugin-thinking")	if errApply != nil {		t.Fatalf("ApplyThinking() error = %v", errApply)	}	if got := gjson.GetBytes(out, "thinking_budget").Int(); got != 10240 {		t.Fatalf("thinking_budget = %d, want 10240; body=%s", got, string(out))	}	if got := gjson.GetBytes(out, "plugin").String(); got != "plugin-thinking" {		t.Fatalf("plugin = %q, want plugin-thinking; body=%s", got, string(out))	}} func TestHostApplyConfigRegistersInterceptorOnlyPlugin(t *testing.T) {	loader := newTestSymbolLoader()	plugin := &testPlugin{		registerResult: pluginapi.Plugin{			Metadata: pluginapi.Metadata{				Name:             "alpha",				Version:          "1.0.0",				Author:           "test",				GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI",			},			Capabilities: pluginapi.Capabilities{				RequestInterceptor: requestInterceptorFunc(func(ctx context.Context, req pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error) {					return pluginapi.RequestInterceptResponse{Body: []byte("registered")}, nil				}),			},		},	}	loader.lookups["alpha"] = newTestSymbolLookup(plugin)	h := NewForTest(loader) 	h.ApplyConfig(context.Background(), &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     makePluginDir(t, "alpha"),			Configs: enabledPluginConfigs("alpha"),		},	}) 	if len(h.activeRecords()) != 1 {		t.Fatalf("Snapshot records = %d, want 1", len(h.activeRecords()))	}} func TestHostApplyConfigDispatchesInterceptorRPCMethods(t *testing.T) {	loader := newTestSymbolLoader()	plugin := &testPlugin{		registerResult: pluginapi.Plugin{			Metadata: pluginapi.Metadata{				Name:             "alpha",				Version:          "1.0.0",				Author:           "test",				GitHubRepository: "https://github.com/router-for-me/CLIProxyAPI",			},			Capabilities: pluginapi.Capabilities{				RequestInterceptor: requestInterceptorFunc(func(ctx context.Context, req pluginapi.RequestInterceptRequest) (pluginapi.RequestInterceptResponse, error) {					return pluginapi.RequestInterceptResponse{Body: []byte("request|rpc")}, nil				}),				ResponseInterceptor: responseInterceptorFunc{					interceptResponse: func(ctx context.Context, req pluginapi.ResponseInterceptRequest) (pluginapi.ResponseInterceptResponse, error) {						return pluginapi.ResponseInterceptResponse{Body: []byte("response|rpc")}, nil					},				},				StreamChunkInterceptor: responseInterceptorFunc{					interceptStreamChunk: func(ctx context.Context, req pluginapi.StreamChunkInterceptRequest) (pluginapi.StreamChunkInterceptResponse, error) {						return pluginapi.StreamChunkInterceptResponse{Body: []byte("chunk|rpc")}, nil					},				},			},		},	}	loader.lookups["alpha"] = newTestSymbolLookup(plugin)	h := NewForTest(loader) 	h.ApplyConfig(context.Background(), &config.Config{		Plugins: config.PluginsConfig{			Enabled: true,			Dir:     makePluginDir(t, "alpha"),			Configs: enabledPluginConfigs("alpha"),		},	}) 	if len(h.activeRecords()) != 1 {		t.Fatalf("Snapshot records = %d, want 1", len(h.activeRecords()))	} 	caps := h.activeRecords()[0].plugin.Capabilities	reqResp, errReq := caps.RequestInterceptor.InterceptRequestBeforeAuth(context.Background(), pluginapi.RequestInterceptRequest{Body: []byte("request")})	if errReq != nil {		t.Fatalf("InterceptRequestBeforeAuth() error = %v", errReq)	}	if got := string(reqResp.Body); got != "request|rpc" {		t.Fatalf("InterceptRequestBeforeAuth() body = %q, want request|rpc", got)	} 	respResp, errResp := caps.ResponseInterceptor.InterceptResponse(context.Background(), pluginapi.ResponseInterceptRequest{Body: []byte("response")})	if errResp != nil {		t.Fatalf("InterceptResponse() error = %v", errResp)	}	if got := string(respResp.Body); got != "response|rpc" {		t.Fatalf("InterceptResponse() body = %q, want response|rpc", got)	} 	chunkResp, errChunk := caps.StreamChunkInterceptor.InterceptStreamChunk(context.Background(), pluginapi.StreamChunkInterceptRequest{Body: []byte("chunk")})	if errChunk != nil {		t.Fatalf("InterceptStreamChunk() error = %v", errChunk)	}	if got := string(chunkResp.Body); got != "chunk|rpc" {		t.Fatalf("InterceptStreamChunk() body = %q, want chunk|rpc", got)	}} func TestInterceptorHelpersReturnErrorsWhenCallbackMissing(t *testing.T) {	if _, errReq := (requestInterceptorFunc(nil)).InterceptRequestBeforeAuth(context.Background(), pluginapi.RequestInterceptRequest{}); errReq == nil {		t.Fatal("InterceptRequestBeforeAuth() error = nil, want missing request interceptor callback")	}	if _, errReq := (requestInterceptorFunc(nil)).InterceptRequestAfterAuth(context.Background(), pluginapi.RequestInterceptRequest{}); errReq == nil {		t.Fatal("InterceptRequestAfterAuth() error = nil, want missing request interceptor callback")	}	if _, errResp := (responseInterceptorFunc{interceptResponse: nil}).InterceptResponse(context.Background(), pluginapi.ResponseInterceptRequest{}); errResp == nil {		t.Fatal("InterceptResponse() error = nil, want missing response interceptor callback")	}	if _, errChunk := (responseInterceptorFunc{interceptStreamChunk: nil}).InterceptStreamChunk(context.Background(), pluginapi.StreamChunkInterceptRequest{}); errChunk == nil {		t.Fatal("InterceptStreamChunk() error = nil, want missing stream chunk interceptor callback")	}} func TestRPCInterceptorsIncludeHostCallbackID(t *testing.T) {	client := &capturePluginClient{}	adapter := &rpcPluginAdapter{		host:   New(),		client: client,	} 	if _, errReq := adapter.InterceptRequestBeforeAuth(context.Background(), pluginapi.RequestInterceptRequest{Body: []byte("request")}); errReq != nil {		t.Fatalf("InterceptRequestBeforeAuth() error = %v", errReq)	}	var req rpcRequestInterceptRequest	if errDecode := json.Unmarshal(client.requests[pluginabi.MethodRequestInterceptBefore], &req); errDecode != nil {		t.Fatalf("decode request interceptor request: %v", errDecode)	}	if req.HostCallbackID == "" {		t.Fatal("request interceptor before-auth host_callback_id is empty")	} 	if _, errReq := adapter.InterceptRequestAfterAuth(context.Background(), pluginapi.RequestInterceptRequest{Body: []byte("request")}); errReq != nil {		t.Fatalf("InterceptRequestAfterAuth() error = %v", errReq)	}	var reqAfter rpcRequestInterceptRequest	if errDecode := json.Unmarshal(client.requests[pluginabi.MethodRequestInterceptAfter], &reqAfter); errDecode != nil {		t.Fatalf("decode after-auth request interceptor request: %v", errDecode)	}	if reqAfter.HostCallbackID == "" {		t.Fatal("request interceptor after-auth host_callback_id is empty")	} 	if _, errResp := adapter.InterceptResponse(context.Background(), pluginapi.ResponseInterceptRequest{Body: []byte("response")}); errResp != nil {		t.Fatalf("InterceptResponse() error = %v", errResp)	}	var resp rpcResponseInterceptRequest	if errDecode := json.Unmarshal(client.requests[pluginabi.MethodResponseInterceptAfter], &resp); errDecode != nil {		t.Fatalf("decode response interceptor request: %v", errDecode)	}	if resp.HostCallbackID == "" {		t.Fatal("response interceptor host_callback_id is empty")	} 	if _, errChunk := adapter.InterceptStreamChunk(context.Background(), pluginapi.StreamChunkInterceptRequest{Body: []byte("chunk")}); errChunk != nil {		t.Fatalf("InterceptStreamChunk() error = %v", errChunk)	}	var chunk rpcStreamChunkInterceptRequest	if errDecode := json.Unmarshal(client.requests[pluginabi.MethodResponseInterceptStreamChunk], &chunk); errDecode != nil {		t.Fatalf("decode stream chunk interceptor request: %v", errDecode)	}

Lines 1–500 of 2305.