Files
HomeAgent/internal/agent/api/provider_security_modal_test.go
JianFeeeee 4e70b8d5d8 refactor(module): 核心仓 Go module 路径改为 github.com/JianFeeeee/HologramAgent
批 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 后缀
2026-10-21 12:17:19 +08:00

138 lines
5.2 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
// 装饰器必须**透传模态能力声明**(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
}