Files
Agent-System/internal/app/chatflow/chatlog_test.go
T
weichenandCursor 18d40fa8a9 feat(chatflow): add ChatLog and ContextAssembler ports
Keep ctxmgr as table owner and filter the authorized visible set in-process so window, stats, restore, digest, and prompt assembly share one Principal-scoped collection without a package move.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 04:11:16 +00:00

146 lines
4.8 KiB
Go

package chatflow_test
import (
"context"
"errors"
"testing"
"chat-agent-system/internal/app/chatflow"
"chat-agent-system/internal/coordinator"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/projectinstance"
)
func setupLog(t *testing.T) (*ctxmgr.ContextManager, *chatflow.Adapter, int64, string) {
t.Helper()
mgr := ctxmgr.NewContextManager(nil, nil, ctxmgr.TimeDecayConfig{})
ctx := context.Background()
const ownerID int64 = 9
sess := "sess-u10-owner"
if _, err := mgr.EnsureChatSession(ctx, sess, ownerID, 42, "U10"); err != nil {
t.Fatal(err)
}
restricted := &projectinstance.StageAccess{
Restricted: true,
ActiveStageKey: "V0.1_triage",
AllowedStageKeys: map[string]bool{"V0.1_triage": true},
}
log := chatflow.NewAdapter(mgr, func(context.Context, int64, int64) (*projectinstance.StageAccess, error) {
return restricted, nil
}, true)
return mgr, log, ownerID, sess
}
func record(t *testing.T, mgr *ctxmgr.ContextManager, sess string, userID int64, content, scopeKind, stageKey string) {
t.Helper()
meta := map[string]any{"scope_kind": scopeKind, "stage_key": stageKey}
projectinstance.ApplyScopeToMetadata(meta, &projectinstance.MessageScope{ScopeKind: scopeKind, StageKey: stageKey})
if _, err := mgr.RecordUserMessageWithID(context.Background(), sess, userID, "user", content, "CASUAL_CHAT", "", meta); err != nil {
t.Fatal(err)
}
}
func TestChatLog_WindowAndStatsSameSet(t *testing.T) {
mgr, log, ownerID, sess := setupLog(t)
ctx := context.Background()
p := chatflow.Principal{UserID: ownerID, Action: chatflow.ActionReadWindow}
record(t, mgr, sess, ownerID, "激活前沟通", projectinstance.ScopePreActivation, "")
record(t, mgr, sess, ownerID, "当前阶段任务", projectinstance.ScopeWorkflowStage, "V0.1_triage")
record(t, mgr, sess, ownerID, "报价阶段秘密", projectinstance.ScopeWorkflowStage, "V1.0_quoting")
win, err := log.GetWindow(ctx, p, ctxmgr.ChatWindowQuery{SessionID: sess, Days: 7})
if err != nil {
t.Fatal(err)
}
n := 0
for _, d := range win.Days {
n += len(d.Messages)
}
if n != 2 {
t.Fatalf("window visible=%d want 2", n)
}
cats, err := log.CategoryStats(ctx, p, sess)
if err != nil {
t.Fatal(err)
}
catTotal := 0
for _, c := range cats {
catTotal += c.Count
}
if catTotal != n {
t.Fatalf("category total %d != window %d", catTotal, n)
}
dates, err := log.DateStats(ctx, p, sess)
if err != nil {
t.Fatal(err)
}
dateTotal := 0
for _, d := range dates {
dateTotal += d.Count
}
if dateTotal != n {
t.Fatalf("date total %d != window %d", dateTotal, n)
}
}
func TestChatLog_RejectsNonOwnerAndQueryIdentity(t *testing.T) {
_, log, _, sess := setupLog(t)
ctx := context.Background()
_, err := log.GetWindow(ctx, chatflow.Principal{UserID: 99, Action: chatflow.ActionReadWindow}, ctxmgr.ChatWindowQuery{SessionID: sess, Days: 7})
if !errors.Is(err, chatflow.ErrForbidden) {
t.Fatalf("guest want forbidden, got %v", err)
}
_, err = log.GetWindow(ctx, chatflow.Principal{}, ctxmgr.ChatWindowQuery{SessionID: sess, Days: 7})
if !errors.Is(err, chatflow.ErrUnauthenticated) {
t.Fatalf("empty principal want unauthenticated, got %v", err)
}
}
func TestChatLog_RestoreAndDigestHideInvisible(t *testing.T) {
mgr, log, ownerID, sess := setupLog(t)
ctx := context.Background()
p := chatflow.Principal{UserID: ownerID, Action: chatflow.ActionAssemble}
record(t, mgr, sess, ownerID, "可见", projectinstance.ScopeWorkflowStage, "V0.1_triage")
record(t, mgr, sess, ownerID, "不可见报价", projectinstance.ScopeWorkflowStage, "V1.0_quoting")
restored, err := log.Restore(ctx, p, sess)
if err != nil {
t.Fatal(err)
}
if len(restored.Messages) != 1 || restored.Messages[0].Content != "可见" {
t.Fatalf("restore leaked invisible: %+v", restored.Messages)
}
digest, err := log.GenerateDigest(ctx, p, sess, restored.Messages[0].DayKey)
if err != nil {
t.Fatal(err)
}
if digest != nil && digest.MessageCount > 1 {
t.Fatalf("digest counted invisible messages: %+v", digest)
}
}
func TestAssembler_OnlyAuthorizedDialogue(t *testing.T) {
mgr, log, ownerID, sess := setupLog(t)
record(t, mgr, sess, ownerID, "可见对话", projectinstance.ScopePreActivation, "")
record(t, mgr, sess, ownerID, "隐藏阶段", projectinstance.ScopeWorkflowStage, "V1.0_quoting")
asm := chatflow.NewAssembler(nil, log)
ctx := context.Background()
userCtx, err := asm.Assemble(ctx, chatflow.Principal{UserID: ownerID}, chatflow.AssembleRequest{
SessionID: sess,
User: coordinator.UserInfo{ID: "u9", Name: "张工", ProjectRole: "composer"},
})
if err != nil {
t.Fatal(err)
}
for _, turn := range userCtx.RecentDialogue {
if turn.Content == "隐藏阶段" {
t.Fatal("assembler injected invisible message into RecentDialogue")
}
}
if len(userCtx.RecentDialogue) == 0 {
t.Fatal("expected visible dialogue in assembled context")
}
}