package transcript import ( "context" "errors" "testing" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // 本文件钉住共享转录映射的行为(网关与渠道桥的 IM 主人私聊回合都经它读历史): // 顺序最旧优先、角色翻译、说话角色归属、文档引用全量还原、条数上限取自本包的 // ContextCount,以及「读不出来就退化为无转录」的降级口径。 // fakeReader 是只实现 Recent 的窄历史视图,并记录每次请求的会话与条数上限。 type fakeReader struct { rows []history.Message err error session string n int callCount int } func (f *fakeReader) Recent(_ context.Context, sessionID string, n int) ([]history.Message, error) { f.session = sessionID f.n = n f.callCount++ if f.err != nil { return nil, f.err } out := make([]history.Message, 0, len(f.rows)) for _, m := range f.rows { if m.SessionID == sessionID { out = append(out, m) } } if n <= 0 { return nil, nil } if len(out) > n { out = out[len(out)-n:] } return out, nil } // textOf 拼接一条模型消息里的文本部件。 func textOf(m model.Message) string { out := "" for _, c := range m.Content { if c.Type == model.ContentTypeText { out += c.Text } } return out } func TestReadMapsRecentRowsOldestFirstWithSpeakerAttribution(t *testing.T) { t.Parallel() rd := &fakeReader{rows: []history.Message{ {ID: "m1", SessionID: "web", Role: history.RoleUser, Content: "喵"}, {ID: "m2", SessionID: "web", Role: history.RoleAssistant, Content: "又用喵来敲门啦", PersonaID: "cyrene"}, {ID: "m3", SessionID: "web", Role: history.RoleSystem, Content: "系统行"}, {ID: "x1", SessionID: "sess-other", Role: history.RoleUser, Content: "别的会话"}, }} got := Read(context.Background(), rd, "web") if len(got) != 3 { t.Fatalf("messages = %d, want 3 (only the requested session): %+v", len(got), got) } if got[0].Role != model.RoleUser || textOf(got[0]) != "喵" { t.Fatalf("messages[0] = %+v, want the oldest user turn first", got[0]) } // 助手话语带上说出它的角色,非助手消息保持已存发送者标签。 if got[1].Role != model.RoleAssistant || got[1].SenderName != "cyrene" { t.Fatalf("messages[1] = %+v, want her reply attributed to cyrene", got[1]) } if got[2].Role != model.RoleSystem { t.Fatalf("messages[2] = %+v, want the persisted system row", got[2]) } } func TestReadWindowIsTheSharedContextCount(t *testing.T) { t.Parallel() rows := make([]history.Message, 0, ContextCount+5) for i := 0; i < ContextCount+5; i++ { rows = append(rows, history.Message{SessionID: "web", Role: history.RoleUser, Content: "第" + string(rune('a'+i)) + "句"}) } rd := &fakeReader{rows: rows} got := Read(context.Background(), rd, "web") // 条数上限只有一个来源:本包的 ContextCount(网关与 IM 车道都从这里取,不许各自造数)。 if rd.n != ContextCount { t.Fatalf("requested window = %d, want the shared ContextCount (%d)", rd.n, ContextCount) } if len(got) != ContextCount { t.Fatalf("messages = %d, want %d (the newest window)", len(got), ContextCount) } } func TestReadDegradesToNil(t *testing.T) { t.Parallel() if got := Read(context.Background(), nil, "web"); got != nil { t.Fatalf("nil store = %+v, want nil", got) } rd := &fakeReader{rows: []history.Message{{SessionID: "web", Role: history.RoleUser, Content: "喵"}}} if got := Read(context.Background(), rd, ""); got != nil { t.Fatalf("empty session = %+v, want nil", got) } if rd.callCount != 0 { t.Fatalf("empty session read the store %d times, want 0", rd.callCount) } bad := &fakeReader{err: errors.New("boom")} if got := Read(context.Background(), bad, "web"); got != nil { t.Fatalf("read error = %+v, want nil (the turn must still run)", got) } } func TestMessageRestoresEveryDocumentPartOnUserRows(t *testing.T) { t.Parallel() raw, err := history.EncodeDocuments([]history.DocumentRef{ {Name: "0123456789abcdef0123456789abcdef.txt", MIME: "text/plain", Size: 12}, {Name: "fedcba9876543210fedcba9876543210.pdf", MIME: "application/pdf", Size: 34}, }) if err != nil { t.Fatalf("EncodeDocuments: %v", err) } msg := Message(history.Message{ID: "m1", Role: history.RoleUser, Content: "读一下", Document: raw}) if len(msg.Content) != 3 { t.Fatalf("parts = %d, want text + 2 documents: %+v", len(msg.Content), msg.Content) } for i, want := range []history.DocumentRef{ {Name: "0123456789abcdef0123456789abcdef.txt", MIME: "text/plain", Size: 12}, {Name: "fedcba9876543210fedcba9876543210.pdf", MIME: "application/pdf", Size: 34}, } { part := msg.Content[i+1] if part.DocumentURL != DocumentPathPrefix+want.Name || part.DocumentMIME != want.MIME { t.Fatalf("document part[%d] = %+v, want %s%s", i, part, DocumentPathPrefix, want.Name) } } // 非用户行上的文档部件会让编码器 fail-loud,因此刻意省掉(坏数据不该卡死会话)。 other := Message(history.Message{ID: "m2", Role: history.RoleAssistant, Content: "好", Document: raw}) for _, c := range other.Content { if c.DocumentURL != "" { t.Fatalf("assistant row carries a document part: %+v", c) } } // 坏列不 panic、也不产出部件(调用方另行记录 WARN)。 broken := Message(history.Message{ID: "m3", Role: history.RoleUser, Content: "坏列", Document: "{not json"}) if len(broken.Content) != 1 { t.Fatalf("broken document column produced %d parts, want just the text: %+v", len(broken.Content), broken.Content) } } func TestMessageRestoresImages(t *testing.T) { t.Parallel() msg := Message(history.Message{ID: "m1", Role: history.RoleUser, Content: "看图", Image: "data:image/png;base64,AAAA"}) if len(msg.Content) != 2 || msg.Content[1].ImageURL != "data:image/png;base64,AAAA" { t.Fatalf("image part = %+v, want the stored data URI restored", msg.Content) } }