mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 14:57:38 +00:00
批 1,不含 SDK 仓与目录改名。 - go.mod 第 1 行 module 路径 - 366 个 .go 文件的完整前缀 github.com/JianFeeeee/HomeAgent 机械替换 (857 处,仅完整前缀;展示名/注释里的 HomeAgent 字样留给批 5) - 未改:third_party 目录名、SDK 自身 go.mod module、go.mod replace 目标、 homed/waiter 可执行名、.hmap 后缀
138 lines
5.2 KiB
Go
138 lines
5.2 KiB
Go
package api
|
||
|
||
// 装饰器必须**透传模态能力声明**(2026-10-17 生产缺陷)。
|
||
//
|
||
// ## 缺陷链(完整复现)
|
||
//
|
||
// 配置 vision=true(本来就是对的)
|
||
// → LuaAdaptedProvider.Vision = true
|
||
// → ProviderManager.Register 包成 *secureProviderDecorator
|
||
// → ProviderSupportsVision 做 p.(ModalProvider) 断言
|
||
// → 装饰器漏实现 SupportsVision ⇒ 断言失败 ⇒ false
|
||
// → 判定"主模型不支持图片"
|
||
// → see_image 的 image_url 被塞进回退链
|
||
// → 找不到视觉源 ⇒ 「没有可用的视觉回退源」
|
||
//
|
||
// 症状极误导:模型明明能看图,内核一口咬定不支持;用户会去怀疑配置、
|
||
// 怀疑缓存、怀疑要重启 —— 全都不对,是装饰器把能力吃掉了。
|
||
//
|
||
// ## 判据怎么咬
|
||
//
|
||
// **走真实的 Register 包装路径**(不是直接构造装饰器),这样如果有人把
|
||
// 包装逻辑改到别处、或新增别的装饰器,判据仍然覆盖。
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/security"
|
||
)
|
||
|
||
// newTestStore 造一个**真正会触发包装**的 store。
|
||
//
|
||
// ★ 关键:WrapProviderForSecurity 在 store == nil 时**直接返回原对象**
|
||
//
|
||
// (见其实现:`if p == nil || store == nil { return p }`)。
|
||
// 我第一版判据传 nil,于是根本没包装,测的是未包装的 stub ⇒ 判据假绿、
|
||
// 变异删掉透传也不红。必须用非 nil store。
|
||
func newTestStore(t *testing.T) *security.Store {
|
||
t.Helper()
|
||
key := make([]byte, 32)
|
||
for i := range key {
|
||
key[i] = byte(i + 1)
|
||
}
|
||
return security.New(security.Options{
|
||
Path: t.TempDir() + "/secrets.db",
|
||
KeyFn: func() ([]byte, error) { return key, nil },
|
||
Enabled: true,
|
||
})
|
||
}
|
||
|
||
// modalStub 同时实现 Provider 与 ModalProvider。
|
||
type modalStub struct {
|
||
name string
|
||
vision, audio bool
|
||
}
|
||
|
||
func (s *modalStub) Name() string { return s.name }
|
||
func (s *modalStub) MaxContextTokens() int { return 1000 }
|
||
func (s *modalStub) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||
return nil, nil
|
||
}
|
||
func (s *modalStub) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||
return nil, nil
|
||
}
|
||
func (s *modalStub) SupportsVision() bool { return s.vision }
|
||
func (s *modalStub) SupportsAudio() bool { return s.audio }
|
||
|
||
// ★★★ 判据:经 Register 包装后,ProviderSupportsVision 仍必须为 true。
|
||
func TestRegisterPreservesVisionCapability(t *testing.T) {
|
||
inner := &modalStub{name: "visionllm", vision: true, audio: false}
|
||
|
||
// 直接包一层装饰器(等价于 Register 内部做的事)
|
||
wrapped := WrapProviderForSecurity(inner, newTestStore(t))
|
||
|
||
// ★ 前置断言:必须真的被包装了,否则下面的测试毫无意义(我踩过这个坑)
|
||
if _, ok := wrapped.(*secureProviderDecorator); !ok {
|
||
t.Fatalf("前置失败:没有被包装(类型 %T)⇒ 判据会假绿", wrapped)
|
||
}
|
||
if !ProviderSupportsVision(wrapped) {
|
||
t.Fatal("★ 装饰器吃掉了 vision 能力 —— " +
|
||
"ProviderManager.Register 包装后 ProviderSupportsVision 变 false," +
|
||
"导致 see_image 的图被误判为需要回退(真事故 2026-10-17)")
|
||
}
|
||
if !wrapped.(ModalProvider).SupportsVision() {
|
||
t.Fatal("★ 断言成 ModalProvider 后 SupportsVision 仍为 false")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:vision=false 的源不能被装饰器"变成" true。
|
||
//
|
||
// 反向保证:透传是照实传,不是一律返回 true(否则不支持视觉的源
|
||
// 会被当成能看,图片原样发出去被上游忽略 ⇒ 又一次静默剥离)。
|
||
func TestRegisterPreservesNonVisionCapability(t *testing.T) {
|
||
inner := &modalStub{name: "textonly", vision: false, audio: false}
|
||
wrapped := WrapProviderForSecurity(inner, newTestStore(t))
|
||
|
||
if ProviderSupportsVision(wrapped) {
|
||
t.Fatal("★ vision=false 的源被装饰器报成能看图 —— 图片会原样发出被上游忽略")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:audio 能力同样必须透传(同一类缺陷)。
|
||
func TestRegisterPreservesAudioCapability(t *testing.T) {
|
||
inner := &modalStub{name: "audiollm", vision: false, audio: true}
|
||
wrapped := WrapProviderForSecurity(inner, newTestStore(t))
|
||
|
||
if !ProviderSupportsAudio(wrapped) {
|
||
t.Fatal("★ 装饰器吃掉了 audio 能力 —— hear_audio 会被误判需要回退")
|
||
}
|
||
if ProviderSupportsVision(wrapped) {
|
||
t.Fatal("audio-only 源不该报能看图")
|
||
}
|
||
}
|
||
|
||
// ★★★ 判据:内层**没有**实现 ModalProvider 时,装饰器不得 panic 且报 false。
|
||
func TestDecoratorHandlesNonModalProvider(t *testing.T) {
|
||
plain := &plainStub{name: "plain"}
|
||
wrapped := WrapProviderForSecurity(plain, newTestStore(t))
|
||
|
||
if ProviderSupportsVision(wrapped) {
|
||
t.Fatal("内层没有 ModalProvider 却报能看图")
|
||
}
|
||
if ProviderSupportsAudio(wrapped) {
|
||
t.Fatal("内层没有 ModalProvider 却报能听音频")
|
||
}
|
||
}
|
||
|
||
type plainStub struct{ name string }
|
||
|
||
func (s *plainStub) Name() string { return s.name }
|
||
func (s *plainStub) MaxContextTokens() int { return 1000 }
|
||
func (s *plainStub) Chat(ctx context.Context, req *CompletionRequest) (*CompletionResponse, error) {
|
||
return nil, nil
|
||
}
|
||
func (s *plainStub) ChatStream(ctx context.Context, req *CompletionRequest) (<-chan StreamChunk, error) {
|
||
return nil, nil
|
||
}
|