388 lines
10 KiB
Go
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))
|
|
}
|
|
}
|