返回 DeepSeek-Reasonix
cache_profile_test.go
根目录 / internal / plugin / cache_profile_test.go
1 package plugin
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9 )
10
11 // TestProfileCacheIsolation pins the cross-version contract from the MCP 2026
12 // PR plan: the legacy profile keeps the shared v2 file; capability-declaring
13 // profiles write an isolated v3 file and treat the legacy file as a miss, so
14 // catalogs negotiated under different client capabilities never masquerade as
15 // each other.
16 func TestProfileCacheIsolation(t *testing.T) {
17 dir := redirectCache(t)
18 spec := sampleSpec()
19 key := SchemaCacheKey(spec)
20
21 legacy := CachedSchema{
22 CacheKey: key,
23 Capabilities: map[string]bool{"tools": true},
24 Tools: []CachedTool{{Name: "legacy_tool", Schema: []byte(`{"type":"object"}`)}},
25 }
26 if err := SaveCachedSchema(spec.Name, legacy); err != nil {
27 t.Fatalf("save legacy: %v", err)
28 }
29
30 if _, ok := LoadCachedSchemaForSpecProfile(spec, HostProfileInteractive); ok {
31 t.Fatal("interactive profile read the legacy shared cache; must be a miss")
32 }
33 if _, ok := LoadCachedSchemaForSpecProfile(spec, HostProfileDesktopApps); ok {
34 t.Fatal("desktop profile read the legacy shared cache; must be a miss")
35 }
36 if cs, ok := LoadCachedSchemaForSpecProfile(spec, HostProfileCore); !ok || len(cs.Tools) != 1 {
37 t.Fatal("core profile must keep reading the legacy shared cache")
38 }
39
40 enhanced := CachedSchema{
41 CacheKey: key,
42 Capabilities: map[string]bool{"tools": true},
43 Tools: []CachedTool{{
44 Name: "app_tool", Schema: []byte(`{"type":"object"}`),
45 Visibility: []string{"model", "app"}, UIResourceURI: "ui://app/index.html",
46 }},
47 }
48 if err := SaveCachedSchemaForProfile(HostProfileDesktopApps, spec.Name, enhanced); err != nil {
49 t.Fatalf("save enhanced: %v", err)
50 }
51
52 files, err := filepath.Glob(filepath.Join(dir, "mcp", "my-server*"))
53 if err != nil {
54 t.Fatal(err)
55 }
56 if len(files) != 2 {
57 t.Fatalf("expected exactly legacy + enhanced cache files, got %v", files)
58 }
59 var sawEnhanced bool
60 for _, f := range files {
61 if filepath.Base(f) == "my-server.json" {
62 continue
63 }
64 sawEnhanced = true
65 b, err := os.ReadFile(f)
66 if err != nil {
67 t.Fatal(err)
68 }
69 var cs CachedSchema
70 if err := json.Unmarshal(b, &cs); err != nil {
71 t.Fatal(err)
72 }
73 if cs.Version != enhancedCacheVersion || cs.Profile != HostProfileDesktopApps.String() {
74 t.Fatalf("enhanced cache version/profile = %d/%q", cs.Version, cs.Profile)
75 }
76 }
77 if !sawEnhanced {
78 t.Fatal("enhanced cache file not created")
79 }
80
81 cs, ok := LoadCachedSchemaForSpecProfile(spec, HostProfileDesktopApps)
82 if !ok || len(cs.Tools) != 1 || cs.Tools[0].Name != "app_tool" {
83 t.Fatalf("desktop profile cannot load its own enhanced cache: %+v", cs)
84 }
85 if !cs.Tools[0].ToolIsModelVisible() {
86 t.Fatal("model+app visibility must stay model-visible")
87 }
88 if cs, ok := LoadCachedSchemaForSpecProfile(spec, HostProfileInteractive); ok {
89 t.Fatalf("interactive profile read the desktop cache: %+v", cs)
90 }
91 if cs, ok := LoadCachedSchemaForSpecProfile(spec, HostProfileCore); !ok || cs.Tools[0].Name != "legacy_tool" {
92 t.Fatal("legacy writer's cache was disturbed by the enhanced writer")
93 }
94 }
95
96 // TestEnhancedCacheFutureVersionIsMiss ensures a newer layout is a miss, not a
97 // crash or a silent reuse.
98 func TestEnhancedCacheFutureVersionIsMiss(t *testing.T) {
99 redirectCache(t)
100 spec := sampleSpec()
101 path := cachePathForProfile(spec.Name, HostProfileDesktopApps)
102 if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
103 t.Fatal(err)
104 }
105 future := CachedSchema{
106 Version: enhancedCacheVersion + 1,
107 Profile: HostProfileDesktopApps.String(),
108 CacheKey: SchemaCacheKey(spec),
109 Capabilities: map[string]bool{"tools": true},
110 Tools: []CachedTool{{Name: "future", Schema: []byte(`{"type":"object"}`)}},
111 }
112 b, err := json.Marshal(future)
113 if err != nil {
114 t.Fatal(err)
115 }
116 if err := os.WriteFile(path, b, 0o600); err != nil {
117 t.Fatal(err)
118 }
119 if _, ok := LoadCachedSchemaForSpecProfile(spec, HostProfileDesktopApps); ok {
120 t.Fatal("future cache version must be a miss")
121 }
122 }
123
124 // TestEnhancedCacheLegacyToolVisibility pins the default visibility rule:
125 // a tool with no visibility metadata stays model-visible.
126 func TestEnhancedCacheLegacyToolVisibility(t *testing.T) {
127 redirectCache(t)
128 tool := CachedTool{Name: "plain", Schema: []byte(`{"type":"object"}`)}
129 if !tool.ToolIsModelVisible() {
130 t.Fatal("absent visibility must default to model-visible")
131 }
132 appOnly := CachedTool{Name: "app-only", Visibility: []string{"app"}}
133 if appOnly.ToolIsModelVisible() {
134 t.Fatal("app-only visibility must be hidden from the model catalog")
135 }
136 }
137
138 // TestProfileHashStableAndDistinct guards the cache filename identity.
139 func TestProfileHashStableAndDistinct(t *testing.T) {
140 interactive := HostProfileInteractive.ProfileCacheHash()
141 desktop := HostProfileDesktopApps.ProfileCacheHash()
142 core := HostProfileCore.ProfileCacheHash()
143 if interactive == desktop || desktop == core || interactive == core {
144 t.Fatalf("profile hashes collided: %s %s %s", core, interactive, desktop)
145 }
146 if HostProfileInteractive.ProfileCacheHash() != interactive {
147 t.Fatal("profile hash not stable")
148 }
149 if len(interactive) != 12 {
150 t.Fatalf("hash length = %d, want 12", len(interactive))
151 }
152 }
153
154 // TestSaveEnhancedSetsTimestamp ensures LastValidated is stamped exactly once.
155 func TestSaveEnhancedSetsTimestamp(t *testing.T) {
156 redirectCache(t)
157 spec := sampleSpec()
158 before := time.Now().UTC().Add(-time.Second)
159 if err := SaveCachedSchemaForProfile(HostProfileInteractive, spec.Name, CachedSchema{
160 CacheKey: SchemaCacheKey(spec),
161 Capabilities: map[string]bool{"tools": true},
162 }); err != nil {
163 t.Fatal(err)
164 }
165 cs, ok := LoadCachedSchemaForSpecProfile(spec, HostProfileInteractive)
166 if !ok {
167 t.Fatal("load after save failed")
168 }
169 if cs.LastValidated.Before(before) {
170 t.Fatalf("LastValidated = %v, want >= %v", cs.LastValidated, before)
171 }
172 }
173
173 lines GO