Files
dingtalk-workspace-cli/internal/cache/store_test.go
T
2026-03-27 17:55:57 +08:00

388 lines
10 KiB
Go

// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package cache
import (
"path/filepath"
"testing"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport"
)
func TestRegistrySnapshotFreshness(t *testing.T) {
t.Parallel()
root := t.TempDir()
now := time.Date(2026, 3, 21, 0, 0, 0, 0, time.UTC)
store := NewStore(root)
store.Now = func() time.Time { return now }
err := store.SaveRegistry("tenant/default", RegistrySnapshot{
SavedAt: now,
Servers: []market.ServerDescriptor{{Key: "doc", DisplayName: "文档"}},
})
if err != nil {
t.Fatalf("SaveRegistry() error = %v", err)
}
_, freshness, err := store.LoadRegistry("tenant/default")
if err != nil {
t.Fatalf("LoadRegistry() error = %v", err)
}
if freshness != FreshnessFresh {
t.Fatalf("LoadRegistry() freshness = %s, want %s", freshness, FreshnessFresh)
}
store.Now = func() time.Time { return now.Add(25 * time.Hour) }
_, freshness, err = store.LoadRegistry("tenant/default")
if err != nil {
t.Fatalf("LoadRegistry() stale error = %v", err)
}
if freshness != FreshnessStale {
t.Fatalf("LoadRegistry() stale freshness = %s, want %s", freshness, FreshnessStale)
}
if _, err := filepath.Abs(root); err != nil {
t.Fatalf("unexpected temp dir error: %v", err)
}
}
func TestToolsSnapshotRoundTrip(t *testing.T) {
t.Parallel()
store := NewStore(t.TempDir())
err := store.SaveTools("tenant/default", "doc", ToolsSnapshot{
ServerKey: "doc",
ProtocolVersion: "2025-03-26",
Tools: []transport.ToolDescriptor{
{Name: "create_document", Title: "创建文档"},
},
})
if err != nil {
t.Fatalf("SaveTools() error = %v", err)
}
snapshot, _, err := store.LoadTools("tenant/default", "doc")
if err != nil {
t.Fatalf("LoadTools() error = %v", err)
}
if snapshot.ProtocolVersion != "2025-03-26" {
t.Fatalf("LoadTools() protocol = %q, want 2025-03-26", snapshot.ProtocolVersion)
}
if len(snapshot.Tools) != 1 {
t.Fatalf("LoadTools() len = %d, want 1", len(snapshot.Tools))
}
}
func TestDetailSnapshotUsesDetailTTL(t *testing.T) {
t.Parallel()
now := time.Date(2026, 3, 21, 0, 0, 0, 0, time.UTC)
store := NewStore(t.TempDir())
store.Now = func() time.Time { return now }
if err := store.SaveDetail("tenant/default", "doc", DetailSnapshot{
SavedAt: now,
MCPID: 9629,
Payload: []byte(`{"success":true}`),
}); err != nil {
t.Fatalf("SaveDetail() error = %v", err)
}
_, freshness, err := store.LoadDetail("tenant/default", "doc")
if err != nil {
t.Fatalf("LoadDetail() error = %v", err)
}
if freshness != FreshnessFresh {
t.Fatalf("LoadDetail() freshness = %s, want %s", freshness, FreshnessFresh)
}
store.Now = func() time.Time { return now.Add(DetailTTL + time.Hour) }
_, freshness, err = store.LoadDetail("tenant/default", "doc")
if err != nil {
t.Fatalf("LoadDetail() stale error = %v", err)
}
if freshness != FreshnessStale {
t.Fatalf("LoadDetail() stale freshness = %s, want %s", freshness, FreshnessStale)
}
}
func TestHasActionVersionChanged(t *testing.T) {
t.Parallel()
tests := []struct {
name string
cached map[string]string
detail []market.DetailTool
want bool
}{
{
name: "nil cached returns false",
cached: nil,
detail: []market.DetailTool{{ToolName: "foo", ActionVersion: "v2"}},
want: false,
},
{
name: "empty cached returns false",
cached: map[string]string{},
detail: []market.DetailTool{{ToolName: "foo", ActionVersion: "v2"}},
want: false,
},
{
name: "versions match returns false",
cached: map[string]string{"foo": "v1"},
detail: []market.DetailTool{{ToolName: "foo", ActionVersion: "v1"}},
want: false,
},
{
name: "version changed returns true",
cached: map[string]string{"foo": "v1"},
detail: []market.DetailTool{{ToolName: "foo", ActionVersion: "v2"}},
want: true,
},
{
name: "new tool not in cache returns false",
cached: map[string]string{"foo": "v1"},
detail: []market.DetailTool{
{ToolName: "foo", ActionVersion: "v1"},
{ToolName: "bar", ActionVersion: "v1"},
},
want: false,
},
{
name: "detail with empty version skipped",
cached: map[string]string{"foo": "v1"},
detail: []market.DetailTool{{ToolName: "foo", ActionVersion: ""}},
want: false,
},
{
name: "detail with empty name skipped",
cached: map[string]string{"foo": "v1"},
detail: []market.DetailTool{{ToolName: "", ActionVersion: "v2"}},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := HasActionVersionChanged(tt.cached, tt.detail)
if got != tt.want {
t.Fatalf("HasActionVersionChanged() = %v, want %v", got, tt.want)
}
})
}
}
func TestExtractActionVersions(t *testing.T) {
t.Parallel()
tests := []struct {
name string
tools []market.DetailTool
expect map[string]string
}{
{
name: "nil tools returns nil",
tools: nil,
expect: nil,
},
{
name: "empty tools returns nil",
tools: []market.DetailTool{},
expect: nil,
},
{
name: "extracts versions",
tools: []market.DetailTool{
{ToolName: "create_doc", ActionVersion: "G-ACT-100"},
{ToolName: "search_doc", ActionVersion: "G-ACT-101"},
},
expect: map[string]string{
"create_doc": "G-ACT-100",
"search_doc": "G-ACT-101",
},
},
{
name: "skips empty version",
tools: []market.DetailTool{
{ToolName: "create_doc", ActionVersion: "G-ACT-100"},
{ToolName: "legacy_tool", ActionVersion: ""},
},
expect: map[string]string{
"create_doc": "G-ACT-100",
},
},
{
name: "all empty returns nil",
tools: []market.DetailTool{
{ToolName: "", ActionVersion: "G-ACT-100"},
{ToolName: "tool", ActionVersion: ""},
},
expect: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := ExtractActionVersions(tt.tools)
if len(got) != len(tt.expect) {
t.Fatalf("ExtractActionVersions() len = %d, want %d", len(got), len(tt.expect))
}
for k, v := range tt.expect {
if got[k] != v {
t.Fatalf("ExtractActionVersions()[%q] = %q, want %q", k, got[k], v)
}
}
})
}
}
func TestToolsSnapshotActionVersionsRoundTrip(t *testing.T) {
t.Parallel()
store := NewStore(t.TempDir())
versions := map[string]string{
"create_doc": "G-ACT-100",
"search_doc": "G-ACT-101",
}
err := store.SaveTools("tenant/default", "doc", ToolsSnapshot{
ServerKey: "doc",
ProtocolVersion: "2025-03-26",
Tools: []transport.ToolDescriptor{
{Name: "create_doc", Title: "创建文档"},
},
ActionVersions: versions,
})
if err != nil {
t.Fatalf("SaveTools() error = %v", err)
}
snapshot, _, err := store.LoadTools("tenant/default", "doc")
if err != nil {
t.Fatalf("LoadTools() error = %v", err)
}
if len(snapshot.ActionVersions) != 2 {
t.Fatalf("ActionVersions len = %d, want 2", len(snapshot.ActionVersions))
}
if snapshot.ActionVersions["create_doc"] != "G-ACT-100" {
t.Fatalf("ActionVersions[create_doc] = %q, want G-ACT-100", snapshot.ActionVersions["create_doc"])
}
}
func TestDeleteTools(t *testing.T) {
t.Parallel()
store := NewStore(t.TempDir())
partition := "tenant/default"
err := store.SaveTools(partition, "doc", ToolsSnapshot{
ServerKey: "doc",
Tools: []transport.ToolDescriptor{{Name: "create_doc"}},
})
if err != nil {
t.Fatalf("SaveTools() error = %v", err)
}
if err := store.DeleteTools(partition, "doc"); err != nil {
t.Fatalf("DeleteTools() error = %v", err)
}
_, _, err = store.LoadTools(partition, "doc")
if err == nil {
t.Fatal("LoadTools() should fail after DeleteTools()")
}
}
func TestDeleteToolsNonExistent(t *testing.T) {
t.Parallel()
store := NewStore(t.TempDir())
if err := store.DeleteTools("tenant/default", "nonexistent"); err != nil {
t.Fatalf("DeleteTools(nonexistent) should not error, got %v", err)
}
}
func TestListToolsCacheEntries(t *testing.T) {
t.Parallel()
now := time.Date(2026, 3, 24, 10, 0, 0, 0, time.UTC)
store := NewStore(t.TempDir())
store.Now = func() time.Time { return now }
partition := "tenant/default"
// Save two server tools snapshots
_ = store.SaveTools(partition, "doc", ToolsSnapshot{
ServerKey: "doc",
ProtocolVersion: "2025-03-26",
Tools: []transport.ToolDescriptor{
{Name: "create_doc"},
{Name: "search_doc"},
},
})
_ = store.SaveTools(partition, "calendar", ToolsSnapshot{
ServerKey: "calendar",
ProtocolVersion: "2025-03-26",
Tools: []transport.ToolDescriptor{
{Name: "list_events"},
},
})
entries, err := store.ListToolsCacheEntries(partition)
if err != nil {
t.Fatalf("ListToolsCacheEntries() error = %v", err)
}
if len(entries) != 2 {
t.Fatalf("ListToolsCacheEntries() len = %d, want 2", len(entries))
}
byKey := make(map[string]ToolsCacheEntrySummary, len(entries))
for _, e := range entries {
byKey[e.ServerKey] = e
}
doc, ok := byKey["doc"]
if !ok {
t.Fatal("missing 'doc' in ListToolsCacheEntries()")
}
if doc.Freshness != FreshnessFresh {
t.Fatalf("doc freshness = %s, want %s", doc.Freshness, FreshnessFresh)
}
if doc.ToolCount != 2 {
t.Fatalf("doc tool_count = %d, want 2", doc.ToolCount)
}
cal, ok := byKey["calendar"]
if !ok {
t.Fatal("missing 'calendar' in ListToolsCacheEntries()")
}
if cal.ToolCount != 1 {
t.Fatalf("calendar tool_count = %d, want 1", cal.ToolCount)
}
}
func TestListToolsCacheEntriesEmpty(t *testing.T) {
t.Parallel()
store := NewStore(t.TempDir())
entries, err := store.ListToolsCacheEntries("nonexistent/partition")
if err != nil {
t.Fatalf("ListToolsCacheEntries() error = %v", err)
}
if len(entries) != 0 {
t.Fatalf("ListToolsCacheEntries() len = %d, want 0", len(entries))
}
}