Files
Agent-System/internal/ctxmgr/semantic_summary_test.go
T
WeiCHEN 0c572ddaca test(chat): align semantic shadow tests with async pending path
Drain/wait after GenerateRollingSummary; register summary table writers;
document consolidation discarding duplicate S1 dirty worktrees.
2026-09-22 21:59:12 +08:00

224 lines
7.8 KiB
Go

package ctxmgr
import (
"context"
"errors"
"strings"
"testing"
"time"
)
func TestSemanticSummary_FlagOffUnchanged(t *testing.T) {
t.Cleanup(ResetSummaryFlagsForTest)
SetSummaryFlagsForTest(SummaryFeatureFlags{Shadow: false, Replace: false})
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
mgr.SetSemanticSummarizer(&StubSemanticSummarizer{Narrative: "SHOULD_NOT_APPEAR"})
ctx := context.Background()
sessID := "sess_flag_off"
batch := []ChatMessageRecord{
{ID: 1, Role: "user", Content: "我们准备研发高频超声手术刀系统,请教立项建议", CreatedAt: time.Now()},
{ID: 2, Role: "assistant", Content: "需关注电气安全。", CreatedAt: time.Now()},
}
sum, err := mgr.GenerateRollingSummary(ctx, sessID, nil, batch)
if err != nil {
t.Fatal(err)
}
if sum.Shadow != nil {
t.Fatalf("flag off must not attach shadow: %+v", sum.Shadow)
}
if sum.FreshnessNote != "" {
t.Fatalf("flag off must not set freshness: %q", sum.FreshnessNote)
}
if strings.Contains(sum.Narrative, "SHOULD_NOT_APPEAR") {
t.Fatalf("stub narrative leaked into served narrative: %s", sum.Narrative)
}
if sum.Source != "deterministic" {
t.Fatalf("source=%q want deterministic", sum.Source)
}
}
func TestSemanticSummary_ShadowOKKeepsDeterministic(t *testing.T) {
t.Cleanup(ResetSummaryFlagsForTest)
SetSummaryFlagsForTest(SummaryFeatureFlags{Shadow: true, Replace: true}) // REPLACE env true but unwired
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
mgr.SetSemanticSummarizer(&StubSemanticSummarizer{Narrative: "语义影子叙事:超声刀立项"})
ctx := context.Background()
batch := []ChatMessageRecord{
{ID: 1, Role: "user", Content: "我们准备研发高频超声手术刀系统,请教立项建议", CreatedAt: time.Now()},
{ID: 2, Role: "assistant", Content: "超声手术设备属于二类或三类器械。", CreatedAt: time.Now()},
{ID: 3, Role: "user", Content: "目标市场是国内及欧洲CE,项目预算大约在200万左右,型检周期通常需要多久?", CreatedAt: time.Now()},
}
sum, err := mgr.GenerateRollingSummary(ctx, "sess_shadow_ok", nil, batch)
if err != nil {
t.Fatal(err)
}
if sum.Shadow == nil {
t.Fatal("expected advisory shadow attachment")
}
if strings.Contains(sum.Narrative, "语义影子叙事") {
t.Fatalf("REPLACE must stay unwired; narrative replaced: %s", sum.Narrative)
}
if sum.Facts["device_type"] != "超声手术设备" {
t.Fatalf("deterministic facts lost: %+v", sum.Facts)
}
// Production path is async: instant response is pending; drain / wait for OK.
if sum.Shadow.Status == ShadowStatusPending {
_ = mgr.DrainSemanticShadowTasks(ctx)
if !mgr.WaitShadowIdle(2 * time.Second) {
t.Fatal("shadow worker did not become idle")
}
sum, err = mgr.GetChatSummary(ctx, "sess_shadow_ok")
if err != nil || sum == nil {
t.Fatalf("get after drain: %v %+v", err, sum)
}
}
if sum.Shadow == nil || sum.Shadow.Status != ShadowStatusOK {
t.Fatalf("expected shadow ok after drain, got %+v", sum.Shadow)
}
if sum.Shadow.Narrative != "语义影子叙事:超声刀立项" {
t.Fatalf("shadow narrative mismatch: %q", sum.Shadow.Narrative)
}
if sum.Shadow.ToolPermissions != SemanticToolPermissions {
t.Fatalf("tool permissions=%q want none", sum.Shadow.ToolPermissions)
}
if sum.Shadow.InputDigest == "" {
t.Fatal("missing input digest")
}
if strings.Contains(sum.Narrative, "语义影子叙事") {
t.Fatalf("REPLACE must stay unwired after async attach: %s", sum.Narrative)
}
if sum.FreshnessNote != "" {
t.Fatalf("ok path should clear freshness note: %q", sum.FreshnessNote)
}
}
func TestSemanticSummary_ShadowFailureKeepsDeterministicWithFreshness(t *testing.T) {
t.Cleanup(ResetSummaryFlagsForTest)
SetSummaryFlagsForTest(SummaryFeatureFlags{Shadow: true})
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
mgr.SetSemanticSummarizer(&StubSemanticSummarizer{Err: errors.New("model timeout")})
ctx := context.Background()
batch := []ChatMessageRecord{
{ID: 1, Role: "user", Content: "我们准备研发高频超声手术刀系统,请教立项建议", CreatedAt: time.Now()},
}
sum, err := mgr.GenerateRollingSummary(ctx, "sess_shadow_fail", nil, batch)
if err != nil {
t.Fatal(err)
}
if sum.Shadow == nil {
t.Fatal("expected advisory shadow attachment")
}
if sum.Narrative == "" {
t.Fatal("deterministic narrative must be retained")
}
if strings.Contains(sum.Narrative, "model timeout") {
t.Fatal("error text must not pollute narrative")
}
if sum.Shadow.Status == ShadowStatusPending {
_ = mgr.DrainSemanticShadowTasks(ctx)
if !mgr.WaitShadowIdle(2 * time.Second) {
t.Fatal("shadow worker did not become idle")
}
sum, err = mgr.GetChatSummary(ctx, "sess_shadow_fail")
if err != nil || sum == nil {
t.Fatalf("get after drain: %v %+v", err, sum)
}
}
if sum.Shadow == nil || sum.Shadow.Status != ShadowStatusFallback {
t.Fatalf("expected fallback shadow after drain, got %+v", sum.Shadow)
}
if sum.FreshnessNote == "" || !strings.Contains(sum.FreshnessNote, "deterministic retained") {
t.Fatalf("expected freshness note, got %q", sum.FreshnessNote)
}
}
func TestSemanticSummary_MinimizeDropsMetadataAndCaps(t *testing.T) {
msgs := make([]ChatMessageRecord, 0, 50)
for i := 1; i <= 50; i++ {
msgs = append(msgs, ChatMessageRecord{
ID: int64(i),
Role: "user",
Content: strings.Repeat("字", 400) + " secret-meta",
StageKey: "V1.0_quoting",
Metadata: map[string]any{"stage_key": "V1.0_quoting", "token": "SECRET"},
})
}
min := MinimizeAuthorizedMessages(msgs)
if len(min) != semanticMaxMessages {
t.Fatalf("len=%d want %d", len(min), semanticMaxMessages)
}
if min[0].ID != 11 {
t.Fatalf("expected tail window start id=11, got %d", min[0].ID)
}
for _, m := range min {
if utf8Count(m.Content) > semanticMaxContentRunes+3 { // truncateRunes appends "..."
t.Fatalf("content not truncated: %d runes", utf8Count(m.Content))
}
}
digest1 := DigestAuthorizedInput(min)
digest2 := DigestAuthorizedInput(min)
if digest1 == "" || digest1 != digest2 {
t.Fatalf("digest unstable: %q vs %q", digest1, digest2)
}
}
func utf8Count(s string) int {
return len([]rune(s))
}
func TestDigestSemanticShadow_FlagOff(t *testing.T) {
t.Cleanup(ResetSummaryFlagsForTest)
SetSummaryFlagsForTest(SummaryFeatureFlags{Shadow: false})
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
mgr.SetSemanticSummarizer(&StubSemanticSummarizer{Narrative: "shadow-headline"})
ctx := context.Background()
day := time.Now().Format("2006-01-02")
msgs := []ChatMessageRecord{
{ID: 1, Role: "user", Content: "讨论立项", DayKey: day, CreatedAt: time.Now()},
}
d, err := mgr.GenerateDailyDigestFromMessages(ctx, "sess_d", day, msgs)
if err != nil {
t.Fatal(err)
}
if d.Shadow != nil || d.FreshnessNote != "" {
t.Fatalf("flag off digest must omit shadow: %+v note=%q", d.Shadow, d.FreshnessNote)
}
if d.Source != "rule" {
t.Fatalf("source=%q", d.Source)
}
}
func TestDigestSemanticShadow_Fallback(t *testing.T) {
t.Cleanup(ResetSummaryFlagsForTest)
SetSummaryFlagsForTest(SummaryFeatureFlags{Shadow: true})
mgr := NewContextManager(nil, nil, DefaultTimeDecayConfig())
mgr.SetSemanticSummarizer(&StubSemanticSummarizer{Err: errors.New("boom")})
ctx := context.Background()
day := time.Now().Format("2006-01-02")
msgs := []ChatMessageRecord{
{ID: 1, Role: "user", Content: "讨论立项进度", DayKey: day, CreatedAt: time.Now()},
}
d, err := mgr.GenerateDailyDigestFromMessages(ctx, "sess_d2", day, msgs)
if err != nil {
t.Fatal(err)
}
headline := d.Headline
if d.Shadow == nil || d.Shadow.Status != ShadowStatusFallback {
t.Fatalf("want fallback shadow: %+v", d.Shadow)
}
if d.FreshnessNote == "" {
t.Fatal("want freshness note")
}
if d.Headline != headline || d.Headline == "" {
t.Fatal("deterministic headline must remain")
}
}