返回 DeepSeek-Reasonix
freshness_test.go
根目录 / internal / memory / freshness_test.go
1 package memory
2
3 import (
4 "strings"
5 "testing"
6 "time"
7 )
8
9 var freshnessNow = time.Date(2026, 8, 8, 12, 0, 0, 0, time.UTC)
10
11 func agedMemory(age time.Duration) Memory {
12 return Memory{Type: TypeProject, UpdatedAt: freshnessNow.Add(-age)}
13 }
14
15 func TestVolatilityOverridesTypeWindows(t *testing.T) {
16 old := agedMemory(60 * 24 * time.Hour) // past project fresh (30d), inside current (180d)
17 if got := FreshnessFor(old, freshnessNow); got != FreshnessCurrent {
18 t.Fatalf("type default: 60d project fact = %q, want current", got)
19 }
20
21 volatile := old
22 volatile.Volatility = VolatilityVolatile
23 if got := FreshnessFor(volatile, freshnessNow); got != FreshnessStale {
24 t.Fatalf("volatile 60d fact = %q, want stale (7/30d windows)", got)
25 }
26
27 evergreen := agedMemory(3 * 365 * 24 * time.Hour)
28 evergreen.Volatility = VolatilityEvergreen
29 if got := FreshnessFor(evergreen, freshnessNow); got != FreshnessFresh {
30 t.Fatalf("evergreen 3y fact = %q, want fresh forever", got)
31 }
32 }
33
34 func TestExpiryIsAHardBoundaryExcludedFromRecall(t *testing.T) {
35 store := recallTestStore(t)
36 expired := Memory{
37 ID: "mem-branch", Name: "release-branch", Title: "Current release branch",
38 Description: "release/1.21 is the current release branch", Type: TypeProject,
39 Scope: FactScopeProject, Body: "Target release/1.21 for hotfixes.",
40 }
41 expired.UpdatedAt = freshnessNow.Add(-24 * time.Hour)
42 expired.ExpiresAt = freshnessNow.Add(-time.Hour)
43 recallTestWrite(t, store.Dir, expired)
44
45 if got := FreshnessFor(expired, freshnessNow); got != FreshnessExpired {
46 t.Fatalf("past expires_at = %q, want expired", got)
47 }
48 result := AutoRecall(store, "hotfix 应该打到 release branch release/1.21 吗", RecallOptions{Now: freshnessNow})
49 if len(result.Hits) != 0 {
50 t.Fatalf("expired facts must never be auto-recalled: %+v", result.Hits)
51 }
52 // Explicit search still finds it — expiry hides nothing from management.
53 hits, err := searchMemories(t.Context(), store, "release branch", "", "", 8)
54 if err != nil || len(hits) != 1 {
55 t.Fatalf("explicit search should still see expired facts, got %v %v", hits, err)
56 }
57 }
58
59 func TestVerificationRenewsTheFreshnessClock(t *testing.T) {
60 m := agedMemory(200 * 24 * time.Hour) // past project current window: stale
61 if got := FreshnessFor(m, freshnessNow); got != FreshnessStale {
62 t.Fatalf("200d project fact = %q, want stale", got)
63 }
64 m.LastVerifiedAt = freshnessNow.Add(-24 * time.Hour)
65 if got := FreshnessFor(m, freshnessNow); got != FreshnessFresh {
66 t.Fatalf("verified yesterday = %q, want fresh", got)
67 }
68 }
69
70 func TestSaveRoundTripsVolatilityExpiryAndClearExpiry(t *testing.T) {
71 store := recallTestStore(t)
72 saved, err := store.SaveWithOptions(Memory{
73 Name: "windows-port", Description: "current weekly focus",
74 Volatility: VolatilityVolatile, ExpiresAt: freshnessNow.Add(30 * 24 * time.Hour),
75 Body: "User is doing the Windows port this week.",
76 }, SaveOptions{})
77 if err != nil {
78 t.Fatal(err)
79 }
80 loaded, _ := store.Read(saved.Memory.ID)
81 if NormalizeVolatility(string(loaded.Volatility)) != VolatilityVolatile || loaded.ExpiresAt.IsZero() {
82 t.Fatalf("volatility/expiry lost in round trip: %+v", loaded)
83 }
84
85 // An update omitting both preserves them.
86 if _, err := store.SaveWithOptions(Memory{ID: saved.Memory.ID, Description: "current weekly focus v2", Body: "Still on the port."}, SaveOptions{}); err != nil {
87 t.Fatal(err)
88 }
89 kept, _ := store.Read(saved.Memory.ID)
90 if NormalizeVolatility(string(kept.Volatility)) != VolatilityVolatile || kept.ExpiresAt.IsZero() {
91 t.Fatalf("update must inherit volatility/expiry: %+v", kept)
92 }
93
94 // ClearExpiry drops the boundary without touching volatility.
95 if _, err := store.SaveWithOptions(Memory{ID: saved.Memory.ID, Description: "current weekly focus v3", Body: "Port done."}, SaveOptions{ClearExpiry: true}); err != nil {
96 t.Fatal(err)
97 }
98 cleared, _ := store.Read(saved.Memory.ID)
99 if !cleared.ExpiresAt.IsZero() || NormalizeVolatility(string(cleared.Volatility)) != VolatilityVolatile {
100 t.Fatalf("ClearExpiry must drop only the expiry: %+v", cleared)
101 }
102 }
103
104 func TestParseExpiryFormats(t *testing.T) {
105 if _, clear, err := parseExpiry("never"); err != nil || !clear {
106 t.Fatalf("never should clear, got clear=%v err=%v", clear, err)
107 }
108 when, _, err := parseExpiry("2026-09-01")
109 if err != nil || when.IsZero() {
110 t.Fatalf("bare date rejected: %v", err)
111 }
112 if _, _, err := parseExpiry("下个月"); err == nil || !strings.Contains(err.Error(), "RFC3339") {
113 t.Fatalf("invalid expiry must error with format hint, got %v", err)
114 }
115 }
116
116 lines GO