- loop 解析点用结构断言取 SourceFailure 的主机(errors.As 穿透 %w 包装), 不导入 vision 包、不引入方向性依赖;拿不到来源时留空串而不编造。 - 新增 TestImageSourceHost 四例(nil/普通错误/包装后来源/空主机), 变异验证:把返回值改成 "" 该用例必红(已复跑确认)。
101 lines
2.9 KiB
Go
101 lines
2.9 KiB
Go
package loop
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
|
|
)
|
|
|
|
// fakeSourceErr 模拟 vision 包的 SourceFailure:错误链上能报出来源主机。
|
|
type fakeSourceErr struct{ host string }
|
|
|
|
func (e *fakeSourceErr) Error() string { return "取回失败" }
|
|
func (e *fakeSourceErr) ImageSourceHost() string {
|
|
return e.host
|
|
}
|
|
|
|
// TestImageSourceHost 钉住「日志能定位到来源主机」这条契约:错误链上
|
|
// 带 ImageSourceHost 的证据要穿透 %w 包装被取出;其它错误一律空串
|
|
// (空串表示无从定位,而不是假装有来源)。
|
|
func TestImageSourceHost(t *testing.T) {
|
|
t.Parallel()
|
|
cases := []struct {
|
|
name string
|
|
err error
|
|
want string
|
|
}{
|
|
{name: "nil 错误", err: nil, want: ""},
|
|
{name: "普通错误", err: errors.New("解析失败"), want: ""},
|
|
{name: "包装后的来源错误", err: fmt.Errorf("resolve: %w", &fakeSourceErr{host: "cdn.example.com"}), want: "cdn.example.com"},
|
|
{name: "包装但主机为空", err: fmt.Errorf("resolve: %w", &fakeSourceErr{}), want: ""},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
if got := imageSourceHost(c.err); got != c.want {
|
|
t.Fatalf("imageSourceHost = %q, want %q", got, c.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRun_ImageResolverFoldsToText(t *testing.T) {
|
|
t.Parallel()
|
|
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Model: "m", Content: "Done."}}}}
|
|
a := newTestAgent(t, ad, nil)
|
|
l := New(Options{
|
|
Agent: a,
|
|
MaxRounds: 8,
|
|
Vision: ImageResolveFunc(func(_ context.Context, _ model.Message) (model.Message, error) {
|
|
return model.TextMessage(model.RoleUser, "图中是一只猫"), nil
|
|
}),
|
|
})
|
|
|
|
in := []model.Message{model.ImageMessage(model.RoleUser, "", "data:image/png;base64,AAAA")}
|
|
if _, err := l.Run(context.Background(), "sess-1", in); err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if len(ad.reqs) == 0 {
|
|
t.Fatalf("no model request recorded")
|
|
}
|
|
var userSeen bool
|
|
for _, m := range ad.reqs[0].Messages {
|
|
if m.Role == model.RoleUser {
|
|
userSeen = true
|
|
if messageHasImage(m) {
|
|
t.Fatalf("image still reached the model: %+v", m)
|
|
}
|
|
}
|
|
}
|
|
if !userSeen {
|
|
t.Fatalf("no user message seen by the model")
|
|
}
|
|
}
|
|
|
|
func TestRun_ImageKeptWithoutResolver(t *testing.T) {
|
|
t.Parallel()
|
|
ad := &mockAdapter{chat: []mgResp{{resp: model.Response{Model: "m", Content: "Done."}}}}
|
|
a := newTestAgent(t, ad, nil)
|
|
l := New(Options{Agent: a, MaxRounds: 8})
|
|
|
|
in := []model.Message{model.ImageMessage(model.RoleUser, "look", "data:image/png;base64,AAAA")}
|
|
if _, err := l.Run(context.Background(), "sess-1", in); err != nil {
|
|
t.Fatalf("Run: %v", err)
|
|
}
|
|
if len(ad.reqs) == 0 {
|
|
t.Fatalf("no model request recorded")
|
|
}
|
|
var sawImage bool
|
|
for _, m := range ad.reqs[0].Messages {
|
|
if m.Role == model.RoleUser && messageHasImage(m) {
|
|
sawImage = true
|
|
}
|
|
}
|
|
if !sawImage {
|
|
t.Fatalf("expected the image part to pass through without a resolver")
|
|
}
|
|
}
|