Files
Cyrene_v2/internal/config/runtimeconfig/plugins_test.go
T
AskaEth 29aa60d0eb test(platform): 修 Windows 上平台脆弱的断言(不削弱测试意图)
- trace/task: 亚 tick 耗时合法读到 0(本机时钟步长约 0.44ms),改为断言非负且 Duration == EndedAt-StartedAt
- user/runtimeconfig: Windows 无法表示普通文件的 POSIX 权限位(Stat 恒 0666),0600 断言仅在非 Windows 生效,注释写明实现确实请求 0600
- exec: MSYS pwd 把同一目录渲染成 /tmp/...;改用「标记文件相对名解析」+「与显式 root 的 pwd 输出逐字相等」两条跨平台断言 + 变异验证
- 只读排查全仓同类脆弱断言,结果记入 DevDocs/工作小结
2026-10-03 16:08:52 +08:00

189 lines
5.6 KiB
Go

package runtimeconfig
import (
"encoding/json"
"errors"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestLoadPlugins_MissingFileIsEmpty(t *testing.T) {
t.Parallel()
got, err := LoadPlugins(filepath.Join(t.TempDir(), "plugins.json"))
if err != nil {
t.Fatalf("LoadPlugins: %v", err)
}
if len(got) != 0 {
t.Fatalf("len = %d, want 0", len(got))
}
}
func TestSaveLoadPlugins_RoundTrip(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "nested", "plugins.json")
in := []PluginConfig{{
ID: "p1",
Name: "calc",
Kind: PluginKindMCP,
Transport: TransportHTTP,
Endpoint: "http://127.0.0.1:3001/mcp",
APIKey: "sk-secret-1234",
CreatedAt: "2026-09-08T00:00:00Z",
}}
if err := SavePlugins(path, in); err != nil {
t.Fatalf("SavePlugins: %v", err)
}
info, err := os.Stat(path)
if err != nil {
t.Fatalf("stat: %v", err)
}
if runtime.GOOS != "windows" {
// Windows has no POSIX permission bits for regular files: Chmod only
// toggles the read-only flag and Stat reports 0666 regardless of the
// requested 0600, so this assertion is only meaningful on Unix-like
// platforms. SavePlugins still requests 0600 (see plugins.go).
if perm := info.Mode().Perm(); perm != 0o600 {
t.Fatalf("mode = %o, want 600", perm)
}
}
got, err := LoadPlugins(path)
if err != nil {
t.Fatalf("LoadPlugins: %v", err)
}
if len(got) != 1 || got[0].Name != "calc" || got[0].APIKey != "sk-secret-1234" {
t.Fatalf("got = %+v", got)
}
}
func TestLoadPlugins_EmptyArrayIsValid(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "plugins.json")
if err := os.WriteFile(path, []byte("[]"), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
got, err := LoadPlugins(path)
if err != nil {
t.Fatalf("LoadPlugins: %v", err)
}
if len(got) != 0 {
t.Fatalf("len = %d, want 0", len(got))
}
}
func TestLoadPlugins_InvalidReturnsError(t *testing.T) {
t.Parallel()
path := filepath.Join(t.TempDir(), "plugins.json")
if err := os.WriteFile(path, []byte(`[{"name":""}]`), 0o600); err != nil {
t.Fatalf("write: %v", err)
}
if _, err := LoadPlugins(path); !errors.Is(err, ErrInvalidPlugin) {
t.Fatalf("err = %v, want ErrInvalidPlugin", err)
}
}
func TestValidatePlugin(t *testing.T) {
t.Parallel()
cases := []struct {
name string
cfg PluginConfig
wantErr bool
}{
{name: "stdio ok", cfg: PluginConfig{Name: "a", Command: "mcp-a"}},
{name: "http ok", cfg: PluginConfig{Name: "b", Endpoint: "http://x/mcp"}},
{name: "sse ok", cfg: PluginConfig{Name: "c", Transport: TransportSSE, Endpoint: "http://x/sse"}},
{name: "blank name", cfg: PluginConfig{Command: "x"}, wantErr: true},
{name: "bad kind", cfg: PluginConfig{Name: "d", Kind: "tool", Command: "x"}, wantErr: true},
{name: "stdio no command", cfg: PluginConfig{Name: "e", Transport: TransportStdio}, wantErr: true},
{name: "http no endpoint", cfg: PluginConfig{Name: "f", Transport: TransportHTTP}, wantErr: true},
{name: "bad transport", cfg: PluginConfig{Name: "g", Transport: "carrier-pigeon", Endpoint: "http://x"}, wantErr: true},
{name: "no transport no fields", cfg: PluginConfig{Name: "h"}, wantErr: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
err := tc.cfg.Validate()
if tc.wantErr && err == nil {
t.Fatal("want error, got nil")
}
if !tc.wantErr && err != nil {
t.Fatalf("unexpected error: %v", err)
}
})
}
}
func TestUpsertPlugin_ByNameReplacesAndPreservesOrder(t *testing.T) {
t.Parallel()
base := []PluginConfig{
{ID: "1", Name: "a", Command: "a"},
{ID: "2", Name: "b", Command: "b"},
}
got := UpsertPlugin(base, PluginConfig{Name: "b", Command: "b2"})
if len(got) != 2 {
t.Fatalf("len = %d, want 2", len(got))
}
if got[1].Name != "b" || got[1].Command != "b2" {
t.Fatalf("got[1] = %+v", got[1])
}
got = UpsertPlugin(got, PluginConfig{Name: "c", Command: "c"})
if len(got) != 3 || got[2].Name != "c" || got[2].ID == "" {
t.Fatalf("append = %+v", got)
}
}
func TestDeletePlugin(t *testing.T) {
t.Parallel()
base := []PluginConfig{{ID: "1", Name: "a", Command: "a"}, {ID: "2", Name: "b", Command: "b"}}
got, removed := DeletePlugin(base, "a")
if !removed || len(got) != 1 || got[0].Name != "b" {
t.Fatalf("removed=%v got=%+v", removed, got)
}
got, removed = DeletePlugin(base, "2")
if !removed || len(got) != 1 || got[0].Name != "a" {
t.Fatalf("by id: removed=%v got=%+v", removed, got)
}
if _, removed := DeletePlugin(base, "nope"); removed {
t.Fatal("removed = true for unknown name")
}
}
func TestMaskedPlugin_EmptySecretsStayEmpty(t *testing.T) {
t.Parallel()
got := MaskedPlugin(PluginConfig{Name: "calc", Env: map[string]string{"API_KEY": ""}})
if got.APIKey != "" {
t.Fatalf("apiKey = %q, want empty", got.APIKey)
}
if got.Env["API_KEY"] != "" {
t.Fatalf("empty env secret = %q, want empty", got.Env["API_KEY"])
}
}
func TestMaskedPlugin_RedactsKeyAndEnv(t *testing.T) {
t.Parallel()
p := PluginConfig{
Name: "calc",
APIKey: "sk-secret-1234",
Env: map[string]string{"PATH": "/usr/bin", "GITHUB_TOKEN": "ghp-abcdefgh"},
}
got := MaskedPlugin(p)
if got.APIKey != "****1234" {
t.Fatalf("apiKey = %q, want ****1234", got.APIKey)
}
if got.Env["PATH"] != "/usr/bin" {
t.Fatalf("PATH = %q, want unchanged", got.Env["PATH"])
}
if got.Env["GITHUB_TOKEN"] != "****efgh" {
t.Fatalf("GITHUB_TOKEN = %q, want masked", got.Env["GITHUB_TOKEN"])
}
if p.APIKey != "sk-secret-1234" {
t.Fatal("original mutated")
}
raw, _ := json.Marshal(got)
if strings.Contains(string(raw), "sk-secret-1234") || strings.Contains(string(raw), "ghp-abcdefgh") {
t.Fatalf("masked JSON leaks a secret: %s", raw)
}
}