Files
HomeAgent/internal/agent/api/router_test.go
JianFeeeee ae42e486de feat(pluginmgr): 插件更新接口(upgrade/downgrade 保留配置)+ skill_install overwrite
内核 Registry 拆出 StopAndUnload:
- 停止并从注册表移除插件但保留 config_<name> 表
- 不触发 onRemove 回调(那是删除专用语义)
- RemovePlugin 改为追加清理配置表示清除,更新场景调 StopAndUnload

pluginmgr:
- installFromData/installFromURL/installFromPath 加 overwrite 参数
- 已存在+overwrite=true:StopAndUnload→备份旧目录→解压新包→失败回滚→
  返回 action=upgraded/downgraded/reinstalled+previous_version+config_kept
- 已存在+overwrite=false:返回 error+hint(指向 overwrite 用法)
- cmpVersion 点分版本号数字比较(非字典序)
- 测试覆盖:首次安装→重装拒绝→升级保留配置→降级→失败回滚

skill_install 加 overwrite 参数:
- 同名技能存在时先卸载旧实例+删除目录再安装新包

SDK PluginMgr 接口同步加 StopAndUnload(name string) error

工具链 plugindev 已重建到 /usr/local/bin(7/29→8/25 版本)
QQ 插件诊断日志版(webhook recv 到达+isAtBot 失败日志)已打包并
通过 upgrade 接口热更新部署,配置保留验证通过。
2026-08-25 22:02:17 +08:00

107 lines
3.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"context"
"encoding/json"
"strings"
"testing"
)
// stubRoutableProvider implements both Provider and RoutableProvider.
type stubRoutableProvider struct {
name string
model string
prio int
}
func (s *stubRoutableProvider) Name() string { return s.name }
func (s *stubRoutableProvider) Model() string { return s.model }
func (s *stubRoutableProvider) Priority() int { return s.prio }
func (s *stubRoutableProvider) MaxContextTokens() int { return 4096 }
func (s *stubRoutableProvider) Chat(context.Context, *CompletionRequest) (*CompletionResponse, error) {
return &CompletionResponse{Content: s.name}, nil
}
func (s *stubRoutableProvider) ChatStream(context.Context, *CompletionRequest) (<-chan StreamChunk, error) {
ch := make(chan StreamChunk, 1)
ch <- StreamChunk{Done: true}
close(ch) // 流契约:发送完毕必须关闭 channelaccumulateStream 以此为终止条件)
return ch, nil
}
func TestProviderManagerPriorityOrder(t *testing.T) {
m := NewProviderManager()
m.Register("low", &stubRoutableProvider{name: "low", prio: 10})
m.Register("high", &stubRoutableProvider{name: "high", prio: 90})
m.Register("mid", &stubRoutableProvider{name: "mid", prio: 50})
got := m.OrderedProviders()
wantOrder := []string{"high", "mid", "low"}
for i, p := range got {
if p.Name() != wantOrder[i] {
t.Fatalf("order[%d] = %s, want %s (full=%v)", i, p.Name(), wantOrder[i], names(got))
}
}
}
func TestProviderManagerResolveForModel(t *testing.T) {
m := NewProviderManager()
m.Register("a", &stubRoutableProvider{name: "a", model: "gpt-5"})
m.Register("b", &stubRoutableProvider{name: "b", model: "deepseek"})
got := m.ResolveForModel("deepseek")
if len(got) == 0 || got[0].Name() != "b" {
t.Fatalf("resolve deepseek: got %v", names(got))
}
// 未知模型回落 AUTO 链(仍按优先级)
got2 := m.ResolveForModel("unknown")
if len(got2) == 0 {
t.Fatal("unknown model should fall back to auto chain")
}
}
func names(ps []Provider) []string {
out := make([]string, len(ps))
for i, p := range ps {
out[i] = p.Name()
}
return out
}
func TestMessageAudioBlockToInputAudio(t *testing.T) {
const b64 = "QUJDREVG" // base64 of "ABCDEF"
m := Message{
Role: "user",
Blocks: []ContentBlock{
{Type: "text", Text: "what is this"},
{Type: "audio_url", AudioURL: &AudioURL{URL: "data:audio/wav;base64," + b64}},
},
}
data, err := json.Marshal(m)
if err != nil {
t.Fatalf("marshal: %v", err)
}
s := string(data)
if !strings.Contains(s, `"type":"input_audio"`) {
t.Fatalf("expected input_audio, got: %s", s)
}
if !strings.Contains(s, `"format":"wav"`) || !strings.Contains(s, b64) {
t.Fatalf("missing data/format: %s", s)
}
if strings.Contains(s, `"type":"audio_url"`) {
t.Fatalf("audio_url should be converted: %s", s)
}
}
func TestMessageAudioURLPassthroughWhenNotBase64(t *testing.T) {
m := Message{
Role: "user",
Blocks: []ContentBlock{
{Type: "audio_url", AudioURL: &AudioURL{URL: "https://cdn.example/a.wav"}},
},
}
data, _ := json.Marshal(m)
if !strings.Contains(string(data), `"type":"audio_url"`) {
t.Fatalf("non-base64 audio_url should stay: %s", data)
}
}