Drain/wait after GenerateRollingSummary; register summary table writers; document consolidation discarding duplicate S1 dirty worktrees.
224 lines
7.8 KiB
Go
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")
|
|
}
|
|
}
|