package flowgraph // 本文件是**保留期清扫**的验收(切片 F2)。断言的是库里的事实(哪些行还在), // 与 `internal/runtime/events/gc_test.go` 同一口径。 import ( "context" "testing" "time" ) // TestSweep_DisabledIsNoOp_边界 钉住「配了 ≤0 = 关闭 GC」:`Sweep` 是**空操作**, // 一行都不删。0 刻意不解释成「立即清空」—— 那会让一次笔误抹掉刚落盘的历史轮。 func TestSweep_DisabledIsNoOp_边界(t *testing.T) { t.Parallel() ctx := context.Background() for _, tc := range []struct { name string gcInterval, retain time.Duration }{ {"interval=0", 0, time.Hour}, {"interval<0", -time.Hour, time.Hour}, {"retention=0", time.Hour, 0}, {"retention<0", time.Hour, -time.Hour}, } { t.Run(tc.name, func(t *testing.T) { t.Parallel() st, _ := newTestStore(t, func(c *Config) { c.GCInterval = tc.gcInterval c.Retention = tc.retain }) if st.Enabled() { t.Fatalf("Enabled() = true, want false (%s)", tc.name) } if err := st.RecordRound(sampleGraph("web", 1)); err != nil { t.Fatalf("RecordRound: %v", err) } stats, err := st.Sweep(ctx) if err != nil { t.Fatalf("Sweep: %v", err) } if stats.Deleted != 0 || stats.Batches != 0 { t.Fatalf("disabled Sweep = %+v, want zero", stats) } turns, err := st.Turns(ctx, "web", 0) if err != nil { t.Fatalf("Turns: %v", err) } if len(turns) != 1 { t.Fatalf("行被删了(turns=%d),但 GC 关闭时必须是空操作", len(turns)) } }) } } // TestSweep_DeletesOnlyExpired_正常 钉住「正数只删超期」:保留期内的行**必须**留下, // 只有 `created_at < now − 保留期` 的行被回收。时钟注入让这条判定可复现。 func TestSweep_DeletesOnlyExpired_正常(t *testing.T) { t.Parallel() ctx := context.Background() base := testNow() clock := base st, _ := newTestStore(t, func(c *Config) { c.GCInterval = time.Hour c.Retention = 24 * time.Hour c.Now = func() time.Time { return clock } }) if !st.Enabled() { t.Fatal("Enabled() = false, want true(间隔与保留期都 > 0)") } // 旧轮:T0 落盘。 if err := st.RecordRound(sampleGraph("web", 1)); err != nil { t.Fatalf("record old round: %v", err) } // 时钟拨到 T0+48h,落一个新轮。 clock = base.Add(48 * time.Hour) if err := st.RecordRound(sampleGraph("web", 2)); err != nil { t.Fatalf("record new round: %v", err) } // cutoff = (T0+48h) − 24h = T0+24h ⇒ web-1(T0)超期、web-2(T0+48h)留下。 stats, err := st.Sweep(ctx) if err != nil { t.Fatalf("Sweep: %v", err) } if stats.Deleted != 1 { t.Fatalf("Deleted = %d, want 1(只删超期的那一轮)", stats.Deleted) } turns, err := st.Turns(ctx, "web", 0) if err != nil { t.Fatalf("Turns: %v", err) } if len(turns) != 1 || turns[0].TurnID != "web-2" { t.Fatalf("turns after sweep = %+v, want [web-2](保留期内的那轮必须留下)", turns) } if _, err := st.Graph(ctx, "web", "web-1"); err != nil { t.Fatalf("Graph(被删轮) err = %v, want nil(缺席不是错误)", err) } old, err := st.Graph(ctx, "web", "web-1") if err != nil { t.Fatalf("Graph(web-1): %v", err) } if old != nil { t.Fatalf("web-1 应已被回收,却仍读得到: %+v", old) } } // TestSweep_ScopedPerSession_正常 钉住清扫是**全表按时间**的(不是按会话):过期的 // 会话行照样回收,不会因为另一个会话还有新轮就整表留着。 func TestSweep_ScopedPerSession_正常(t *testing.T) { t.Parallel() ctx := context.Background() base := testNow() clock := base st, _ := newTestStore(t, func(c *Config) { c.GCInterval = time.Hour c.Retention = time.Hour c.Now = func() time.Time { return clock } }) if err := st.RecordRound(sampleGraph("web", 1)); err != nil { t.Fatalf("record web: %v", err) } clock = base.Add(3 * time.Hour) if err := st.RecordRound(sampleGraph("sess-backthink", 1)); err != nil { t.Fatalf("record backthink: %v", err) } if _, err := st.Sweep(ctx); err != nil { t.Fatalf("Sweep: %v", err) } webTurns, err := st.Turns(ctx, "web", 0) if err != nil { t.Fatalf("Turns(web): %v", err) } if len(webTurns) != 0 { t.Fatalf("web turns = %+v, want empty(3h 前的轮已超 1h 保留期)", webTurns) } btTurns, err := st.Turns(ctx, "sess-backthink", 0) if err != nil { t.Fatalf("Turns(backthink): %v", err) } if len(btTurns) != 1 { t.Fatalf("backthink turns = %+v, want [sess-backthink-1]", btTurns) } }