313 lines
8.1 KiB
Go
313 lines
8.1 KiB
Go
// 织忆 MemoryWeave — 自优化层单元测试
|
|
package selfoptimize
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// ─── 仪表盘 ──────────────────────────────────────────────
|
|
|
|
func TestDashboard_InitialMetricsAllZero(t *testing.T) {
|
|
d := &Dashboard{}
|
|
m := d.Metrics()
|
|
if m["recall_usefulness_rate"] != 0 {
|
|
t.Errorf("expected 0, got %f", m["recall_usefulness_rate"])
|
|
}
|
|
if m["recall_hit_rate"] != 0 {
|
|
t.Errorf("expected 0, got %f", m["recall_hit_rate"])
|
|
}
|
|
}
|
|
|
|
func TestDashboard_UsefulnessRate(t *testing.T) {
|
|
d := &Dashboard{}
|
|
d.RecordFeedback(true)
|
|
d.RecordFeedback(true)
|
|
d.RecordFeedback(true)
|
|
d.RecordFeedback(false)
|
|
|
|
m := d.Metrics()
|
|
if m["recall_usefulness_rate"] != 0.75 {
|
|
t.Errorf("expected 0.75, got %f", m["recall_usefulness_rate"])
|
|
}
|
|
}
|
|
|
|
func TestDashboard_HitRate(t *testing.T) {
|
|
d := &Dashboard{}
|
|
d.RecordRecall(true)
|
|
d.RecordRecall(true)
|
|
d.RecordRecall(false)
|
|
|
|
m := d.Metrics()
|
|
if m["recall_hit_rate"] < 0.66 || m["recall_hit_rate"] > 0.67 {
|
|
t.Errorf("expected ~0.67, got %f", m["recall_hit_rate"])
|
|
}
|
|
}
|
|
|
|
func TestDashboard_GapClosureRate(t *testing.T) {
|
|
d := &Dashboard{}
|
|
d.TotalGaps = 10
|
|
d.ClosedGaps = 5
|
|
m := d.Metrics()
|
|
if m["gap_closure_rate"] != 0.5 {
|
|
t.Errorf("expected 0.5, got %f", m["gap_closure_rate"])
|
|
}
|
|
}
|
|
|
|
func TestDashboard_RecordDeprecation(t *testing.T) {
|
|
d := &Dashboard{}
|
|
d.RecordDeprecation()
|
|
d.RecordDeprecation()
|
|
if d.DeprecatedToday != 2 {
|
|
t.Errorf("expected 2, got %d", d.DeprecatedToday)
|
|
}
|
|
}
|
|
|
|
func TestDashboard_RecordCorrection(t *testing.T) {
|
|
d := &Dashboard{}
|
|
d.RecordCorrection("muchen_correction")
|
|
d.RecordCorrection("config_parse")
|
|
if d.TotalFixes != 2 {
|
|
t.Errorf("expected 2 fixes, got %d", d.TotalFixes)
|
|
}
|
|
if d.CascadeFixedTotal != 1 {
|
|
t.Errorf("expected 1 cascade, got %d", d.CascadeFixedTotal)
|
|
}
|
|
}
|
|
|
|
func TestDashboard_RecordConflictResolved(t *testing.T) {
|
|
d := &Dashboard{}
|
|
d.RecordConflictResolved(true)
|
|
d.RecordConflictResolved(false)
|
|
if d.TotalConflicts != 2 {
|
|
t.Errorf("expected 2 conflicts, got %d", d.TotalConflicts)
|
|
}
|
|
if d.AutoResolvedConflicts != 1 {
|
|
t.Errorf("expected 1 auto, got %d", d.AutoResolvedConflicts)
|
|
}
|
|
m := d.Metrics()
|
|
if m["auto_resolve_rate"] != 0.5 {
|
|
t.Errorf("expected 0.5, got %f", m["auto_resolve_rate"])
|
|
}
|
|
}
|
|
|
|
// ─── 知识缺口检测 ────────────────────────────────────────
|
|
|
|
func TestGapDetector_NotTriggeredBeforeThreshold(t *testing.T) {
|
|
gd := NewGapDetector()
|
|
gap := gd.RecordMiss("kubernetes")
|
|
if gap != nil {
|
|
t.Error("should not trigger gap after 1 miss")
|
|
}
|
|
gd.RecordMiss("kubernetes")
|
|
if gap := gd.RecordMiss("kubernetes"); gap == nil {
|
|
t.Error("should trigger gap after 3 misses")
|
|
}
|
|
}
|
|
|
|
func TestGapDetector_DetectsGapAtThreshold(t *testing.T) {
|
|
gd := NewGapDetector()
|
|
gd.RecordMiss("topicX")
|
|
gd.RecordMiss("topicX")
|
|
gap := gd.RecordMiss("topicX")
|
|
|
|
if gap == nil {
|
|
t.Fatal("expected gap after 3 misses")
|
|
}
|
|
if gap.Topic != "topicX" {
|
|
t.Errorf("wrong topic: %s", gap.Topic)
|
|
}
|
|
if gap.MissCount != 3 {
|
|
t.Errorf("expected 3 misses, got %d", gap.MissCount)
|
|
}
|
|
}
|
|
|
|
func TestGapDetector_CloseGap(t *testing.T) {
|
|
gd := NewGapDetector()
|
|
gd.RecordMiss("topic")
|
|
gd.RecordMiss("topic")
|
|
gd.RecordMiss("topic")
|
|
gd.Close("topic")
|
|
|
|
gaps := gd.List()
|
|
if len(gaps) == 0 || !gaps[0].Closed {
|
|
t.Error("gap should be closed")
|
|
}
|
|
}
|
|
|
|
func TestGapDetector_ClassifySynonym(t *testing.T) {
|
|
gd := NewGapDetector()
|
|
gd.RecordMiss("API")
|
|
gd.RecordMiss("API")
|
|
gd.RecordMiss("API")
|
|
|
|
gaps := gd.List()
|
|
if len(gaps) > 0 && gaps[0].Type != GapSynonym {
|
|
t.Errorf("expected synonym gap, got %s", gaps[0].Type)
|
|
}
|
|
}
|
|
|
|
// ─── 因果追踪 ────────────────────────────────────────────
|
|
|
|
func TestCausalTracker_RecordVersions(t *testing.T) {
|
|
ct := NewCausalTracker()
|
|
ct.RecordVersion("mem1", "Docker is used by Hermes", "muchen_oral", "init")
|
|
ct.RecordVersion("mem1", "Docker is used by Hermes AND OpenClaw", "config_parse", "update")
|
|
|
|
if !ct.IsVolatile("mem1") {
|
|
// 2 versions → not volatile (volatile = 3+)
|
|
}
|
|
ct.RecordVersion("mem1", "Docker usage updated", "muchen_correction", "correction")
|
|
if !ct.IsVolatile("mem1") {
|
|
t.Error("mem1 should be volatile after 3 versions")
|
|
}
|
|
}
|
|
|
|
func TestCausalTracker_DependencyChain(t *testing.T) {
|
|
ct := NewCausalTracker()
|
|
ct.AddDependency("memory_A", "memory_B")
|
|
ct.AddDependency("memory_B", "memory_C")
|
|
|
|
affected := ct.GetAffected("memory_C", nil)
|
|
// memory_B depends on C, memory_A depends on B
|
|
foundB := false
|
|
foundA := false
|
|
for _, id := range affected {
|
|
if id == "memory_B" {
|
|
foundB = true
|
|
}
|
|
if id == "memory_A" {
|
|
foundA = true
|
|
}
|
|
}
|
|
if !foundB || !foundA {
|
|
t.Errorf("expected both A and B affected, got %v", affected)
|
|
}
|
|
}
|
|
|
|
func TestCausalTracker_CircularDependency(t *testing.T) {
|
|
ct := NewCausalTracker()
|
|
ct.AddDependency("A", "B")
|
|
ct.AddDependency("B", "A")
|
|
affected := ct.GetAffected("A", nil)
|
|
// Should not loop infinitely
|
|
if len(affected) >= 10 {
|
|
t.Errorf("circular dependency not handled: %d affected", len(affected))
|
|
}
|
|
}
|
|
|
|
// ─── 来源信任度 ──────────────────────────────────────────
|
|
|
|
func TestSourceTrust(t *testing.T) {
|
|
tests := []struct {
|
|
source string
|
|
expect float64
|
|
}{
|
|
{"muchen_oral", 1.0},
|
|
{"muchen_feishu", 0.95},
|
|
{"config_parse", 0.7},
|
|
{"agent_infer", 0.5},
|
|
{"llm_distill", 0.4},
|
|
{"unknown", 0.3},
|
|
}
|
|
for _, tc := range tests {
|
|
if got := SourceTrust(tc.source); got != tc.expect {
|
|
t.Errorf("SourceTrust(%s) = %f, want %f", tc.source, got, tc.expect)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── 记忆预取 ────────────────────────────────────────────
|
|
|
|
func TestPrefetchGraph_CoOccurrence(t *testing.T) {
|
|
pg := NewPrefetchGraph()
|
|
pg.RecordCoAccess("docker", "nginx")
|
|
pg.RecordCoAccess("docker", "nginx")
|
|
pg.RecordCoAccess("docker", "kubernetes")
|
|
|
|
prefetch := pg.GetPrefetch("docker")
|
|
if len(prefetch) < 1 {
|
|
t.Errorf("expected at least 1 prefetch item, got %d", len(prefetch))
|
|
}
|
|
}
|
|
|
|
func TestPrefetchGraph_NothingToPrefetch(t *testing.T) {
|
|
pg := NewPrefetchGraph()
|
|
prefetch := pg.GetPrefetch("nonexistent")
|
|
if len(prefetch) != 0 {
|
|
t.Errorf("expected empty prefetch, got %d", len(prefetch))
|
|
}
|
|
}
|
|
|
|
// ─── 并发安全 ────────────────────────────────────────────
|
|
|
|
func TestDashboard_Concurrent(t *testing.T) {
|
|
d := &Dashboard{}
|
|
done := make(chan bool)
|
|
for i := 0; i < 50; i++ {
|
|
go func(v bool) {
|
|
for j := 0; j < 100; j++ {
|
|
d.RecordFeedback(v)
|
|
d.RecordRecall(v)
|
|
}
|
|
done <- true
|
|
}(i%2 == 0)
|
|
}
|
|
for i := 0; i < 50; i++ {
|
|
<-done
|
|
}
|
|
m := d.Metrics()
|
|
if m["recall_usefulness_rate"] < 0 || m["recall_usefulness_rate"] > 1 {
|
|
t.Errorf("usefulness rate out of bounds: %f", m["recall_usefulness_rate"])
|
|
}
|
|
}
|
|
|
|
// ─── 基准测试 ────────────────────────────────────────────
|
|
|
|
func BenchmarkDashboardMetrics(b *testing.B) {
|
|
d := &Dashboard{}
|
|
d.UsefulCount = 1000
|
|
d.NotUsefulCount = 200
|
|
d.TotalRecalls = 1200
|
|
d.HitCount = 900
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
d.Metrics()
|
|
}
|
|
}
|
|
|
|
func BenchmarkGapDetection(b *testing.B) {
|
|
gd := NewGapDetector()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
gd.RecordMiss("benchmark_topic")
|
|
}
|
|
}
|
|
|
|
func BenchmarkCausalTracking(b *testing.B) {
|
|
ct := NewCausalTracker()
|
|
for i := 0; i < 100; i++ {
|
|
ct.AddDependency("dep"+string(rune('a'+i%26)), "dep"+string(rune('a'+(i+1)%26)))
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
ct.GetAffected("depa", nil)
|
|
}
|
|
}
|
|
|
|
func BenchmarkPrefetch(b *testing.B) {
|
|
pg := NewPrefetchGraph()
|
|
for i := 0; i < 100; i++ {
|
|
pg.RecordCoAccess("main", "item"+string(rune('a'+i%26)))
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
pg.GetPrefetch("main")
|
|
}
|
|
}
|
|
|
|
func init() {
|
|
// Ensure consistent timestamps for snapshot tests
|
|
time.Local = time.UTC
|
|
}
|