This commit is contained in:
2026-06-22 08:55:57 +08:00
parent dbb87823e8
commit 6ade6e8fa9
325 changed files with 41131 additions and 855 deletions
+127 -2
View File
@@ -9,6 +9,7 @@ import (
"context"
"errors"
"io"
"strings"
"sync"
"testing"
@@ -282,7 +283,7 @@ func (f *fakeStorage) Delete(_ context.Context, _ string) error {
// ===== test builder =====
func buildMessageService(repo *msgFakeRepo, hub *fakeHub, cfg MessageServiceConfig) MessageService {
return NewMessageService(repo, newFakeStorage(), hub, noopAuditor{}, discardLogger(), cfg)
return NewMessageService(repo, newFakeStorage(), hub, noopAuditor{}, discardLogger(), cfg, nil)
}
func defaultMsgConfig() MessageServiceConfig {
@@ -457,7 +458,7 @@ func TestMessageService_Send_CapabilityMismatch(t *testing.T) {
a := &Attachment{ID: uuid.New(), UserID: userID, MimeType: "image/png", SizeBytes: 100}
specialRepo.addAttachment(a)
svc := NewMessageService(specialRepo, newFakeStorage(), hub, noopAuditor{}, discardLogger(), defaultMsgConfig())
svc := NewMessageService(specialRepo, newFakeStorage(), hub, noopAuditor{}, discardLogger(), defaultMsgConfig(), nil)
_, _, err := svc.Send(context.Background(), userID, conv.ID, "hi", []uuid.UUID{a.ID})
require.Error(t, err)
@@ -990,6 +991,130 @@ func TestTruncateByTokens_PreservesNewest(t *testing.T) {
require.Equal(t, "hi", result[0].Blocks[0].Text)
}
// ===== composeHistory FK context injection tests =====
// fakeContextReader 是 ContextReader 的测试替身。
type fakeContextReader struct {
brief string
err error
called bool
}
func (f *fakeContextReader) BriefForConversation(_ context.Context, _ *Conversation) (string, error) {
f.called = true
return f.brief, f.err
}
func newMsgSvcWithReader(repo *msgFakeRepo, reader ContextReader) *messageService {
return &messageService{
repo: repo,
storage: newFakeStorage(),
auditor: noopAuditor{},
log: discardLogger(),
mimeWhite: map[string]bool{},
safetyPct: 0.0,
briefReader: reader,
}
}
func TestComposeHistory_FKBriefMergedIntoSystemPrompt(t *testing.T) {
repo := newMsgFakeRepo()
reqID := uuid.New()
conv := newConv(uuid.New(), uuid.New())
conv.SystemPrompt = "STATIC PROMPT"
conv.RequirementID = &reqID
repo.addConversation(conv)
repo.addMessage(&Message{ID: 1, ConversationID: conv.ID, Role: RoleUser, Content: "hi", Status: StatusOK})
reader := &fakeContextReader{brief: "## 当前需求\n需求 #7:导出报表"}
svc := newMsgSvcWithReader(repo, reader)
model := &LLMModel{ContextWindow: 4096, MaxOutputTokens: 1024}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.True(t, reader.called)
require.Contains(t, req.SystemPrompt, "STATIC PROMPT")
require.Contains(t, req.SystemPrompt, "需求 #7:导出报表")
}
func TestComposeHistory_FKBriefCountsAgainstTokenBudget(t *testing.T) {
repo := newMsgFakeRepo()
reqID := uuid.New()
conv := newConv(uuid.New(), uuid.New())
conv.RequirementID = &reqID
repo.addConversation(conv)
// 多条历史消息,预算很小,FK 简报应挤占消息预算导致历史被截断。
for i := int64(1); i <= 5; i++ {
repo.addMessage(&Message{
ID: i, ConversationID: conv.ID, Role: RoleUser,
Content: "message content that consumes tokens in the budget", Status: StatusOK,
})
}
hugeBrief := strings.Repeat("需求上下文 ", 300)
reader := &fakeContextReader{brief: hugeBrief}
svc := newMsgSvcWithReader(repo, reader)
// ContextWindow 紧到只够装下简报 + 少量消息:
// budget = (500-100)*(1-0) = 400 tokens;简报约 450 tokens(已超 budget),
// 剩余预算 < 5 条消息所需 → 消息被截断。
model := &LLMModel{ContextWindow: 500, MaxOutputTokens: 100}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.Contains(t, req.SystemPrompt, "需求上下文")
// 简报占了预算 → 消息被截断(少于 5 条)。
require.Less(t, len(req.Messages), 5)
}
func TestComposeHistory_NilReaderPreservesBehavior(t *testing.T) {
repo := newMsgFakeRepo()
reqID := uuid.New()
conv := newConv(uuid.New(), uuid.New())
conv.SystemPrompt = "ONLY STATIC"
conv.RequirementID = &reqID
repo.addConversation(conv)
repo.addMessage(&Message{ID: 1, ConversationID: conv.ID, Role: RoleUser, Content: "hi", Status: StatusOK})
svc := newMsgSvcWithReader(repo, nil) // 无 reader
model := &LLMModel{ContextWindow: 4096, MaxOutputTokens: 1024}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.Equal(t, "ONLY STATIC", req.SystemPrompt)
}
func TestComposeHistory_NoFKMountSkipsReader(t *testing.T) {
repo := newMsgFakeRepo()
conv := newConv(uuid.New(), uuid.New()) // 无任何 FK 挂载
conv.SystemPrompt = "STATIC"
repo.addConversation(conv)
repo.addMessage(&Message{ID: 1, ConversationID: conv.ID, Role: RoleUser, Content: "hi", Status: StatusOK})
reader := &fakeContextReader{brief: "SHOULD NOT APPEAR"}
svc := newMsgSvcWithReader(repo, reader)
model := &LLMModel{ContextWindow: 4096, MaxOutputTokens: 1024}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.False(t, reader.called, "reader must not be called without FK mount")
require.Equal(t, "STATIC", req.SystemPrompt)
}
func TestComposeHistory_ReaderErrorFallsBackToStatic(t *testing.T) {
repo := newMsgFakeRepo()
reqID := uuid.New()
conv := newConv(uuid.New(), uuid.New())
conv.SystemPrompt = "STATIC FALLBACK"
conv.RequirementID = &reqID
repo.addConversation(conv)
repo.addMessage(&Message{ID: 1, ConversationID: conv.ID, Role: RoleUser, Content: "hi", Status: StatusOK})
reader := &fakeContextReader{err: errs.New(errs.CodeInternal, "boom")}
svc := newMsgSvcWithReader(repo, reader)
model := &LLMModel{ContextWindow: 4096, MaxOutputTokens: 1024}
req, err := svc.composeHistory(context.Background(), conv, model)
require.NoError(t, err)
require.Equal(t, "STATIC FALLBACK", req.SystemPrompt)
}
// ===== mimeAllowed / capabilityAllows helpers =====
func TestMimeAllowed(t *testing.T) {