package extensions_test import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "sort" "strconv" "strings" "sync" "testing" "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/transport" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/test/mock_mcp" "github.com/spf13/cobra" ) func TestCLIAdaptsToProtocolAddDeleteModifyAfterDiscoveryRefresh(t *testing.T) { server := newEvolvingRuntimeMarketServer(t, protocolFixturePhase1(t)) defer server.Close() app.SetDiscoveryBaseURL(server.URL()) t.Cleanup(func() { app.SetDiscoveryBaseURL("") }) t.Setenv("DWS_CACHE_DIR", t.TempDir()) mustRunRoot(t, []string{"cache", "refresh"}) assertProductTools(t, mcpSurface(t), "doc", []string{"create-document", "search-documents"}) assertNoProduct(t, mcpSurface(t), "drive") server.SetFixture(t, protocolFixturePhase2(t)) mustRunRoot(t, []string{"cache", "refresh"}) addedSurface := mcpSurface(t) assertProductTools(t, addedSurface, "doc", []string{"archive-document", "create-document", "search-documents"}) assertProductTools(t, addedSurface, "drive", []string{"list-files"}) server.SetFixture(t, protocolFixturePhase3(t)) mustRunRoot(t, []string{"cache", "refresh"}) updatedSurface := mcpSurface(t) assertProductTools(t, updatedSurface, "doc", []string{"archive-document", "create-doc-v2"}) assertNoProduct(t, updatedSurface, "drive") help := mustRunRoot(t, []string{"mcp", "doc", "create-doc-v2", "--help"}) if !strings.Contains(help, "--folder-id") { t.Fatalf("help output missing --folder-id:\n%s", help) } payload := mustRunRootJSON(t, []string{"mcp", "doc", "create-doc-v2", "--name", "协议升级文档", "--folder-id", "folder-a"}) invocation, ok := payload["invocation"].(map[string]any) if !ok { t.Fatalf("invocation payload missing: %#v", payload) } if invocation["tool"] != "create_document" { t.Fatalf("invocation.tool = %#v, want create_document", invocation["tool"]) } params, ok := invocation["params"].(map[string]any) if !ok { t.Fatalf("invocation.params missing: %#v", invocation["params"]) } if params["title"] != "协议升级文档" { t.Fatalf("params.title = %#v, want 协议升级文档", params["title"]) } if params["folder_id"] != "folder-a" { t.Fatalf("params.folder_id = %#v, want folder-a", params["folder_id"]) } legacyOut, legacyErr := runRoot(t, []string{"mcp", "doc", "create-document", "--title", "legacy"}) if legacyErr == nil { t.Fatal("expected legacy command invocation to fail after protocol update") } legacyText := strings.ToLower(strings.TrimSpace(legacyErr.Error() + "\n" + legacyOut)) if !strings.Contains(legacyText, "unknown command") && !strings.Contains(legacyText, "unknown flag") { t.Fatalf("expected legacy command invocation to fail with unknown command/flag, err=%v output:\n%s", legacyErr, legacyOut) } } func TestCLIKeepsCachedProtocolSurfaceUntilManualRefreshAfterCacheAges(t *testing.T) { server := newEvolvingRuntimeMarketServer(t, protocolFixturePhase1(t)) defer server.Close() app.SetDiscoveryBaseURL(server.URL()) t.Cleanup(func() { app.SetDiscoveryBaseURL("") }) cacheDir := t.TempDir() t.Setenv("DWS_CACHE_DIR", cacheDir) mustRunRoot(t, []string{"cache", "refresh"}) server.SetFixture(t, protocolFixturePhase2(t)) ageCacheSnapshots(t, cacheDir, time.Now().UTC().Add(-2*time.Hour)) root := app.NewRootCommand() mcp := findChild(root, "mcp") if mcp == nil { t.Fatal("mcp command not found in root command tree") } assertProductToolsFromCommand(t, mcp, "doc", []string{"create-document", "search-documents"}) assertNoProductFromCommand(t, mcp, "drive") mustRunRoot(t, []string{"cache", "refresh"}) root = app.NewRootCommand() mcp = findChild(root, "mcp") if mcp == nil { t.Fatal("mcp command not found in root command tree") } assertProductToolsFromCommand(t, mcp, "doc", []string{"archive-document", "create-document", "search-documents"}) assertProductToolsFromCommand(t, mcp, "drive", []string{"list-files"}) } type evolvingRuntimeMarketServer struct { t *testing.T mu sync.RWMutex fixture mockmcp.Fixture server *httptest.Server stats runtimeRequestStats } type runtimeRequestStats struct { registryCalls int detailCalls map[int]int mcpCalls map[string]map[string]int } func newEvolvingRuntimeMarketServer(t *testing.T, fixture mockmcp.Fixture) *evolvingRuntimeMarketServer { t.Helper() s := &evolvingRuntimeMarketServer{t: t} s.SetFixture(t, fixture) mux := http.NewServeMux() mux.HandleFunc("/cli/discovery/apis", s.serveMarketServers) mux.HandleFunc("/mcp/market/detail", s.serveMarketDetail) mux.HandleFunc("/", s.serveMCP) s.server = httptest.NewServer(mux) return s } func (s *evolvingRuntimeMarketServer) Close() { if s.server != nil { s.server.Close() } } func (s *evolvingRuntimeMarketServer) URL() string { if s.server == nil { return "" } return s.server.URL } func (s *evolvingRuntimeMarketServer) SetFixture(t *testing.T, fixture mockmcp.Fixture) { t.Helper() if err := fixture.Validate(); err != nil { t.Fatalf("fixture.Validate() error = %v", err) } s.mu.Lock() defer s.mu.Unlock() s.fixture = cloneFixture(t, fixture) } func (s *evolvingRuntimeMarketServer) ResetStats() { s.mu.Lock() defer s.mu.Unlock() s.stats = runtimeRequestStats{ detailCalls: make(map[int]int), mcpCalls: make(map[string]map[string]int), } } func (s *evolvingRuntimeMarketServer) RegistryCalls() int { s.mu.RLock() defer s.mu.RUnlock() return s.stats.registryCalls } func (s *evolvingRuntimeMarketServer) DetailCalls(mcpID int) int { s.mu.RLock() defer s.mu.RUnlock() return s.stats.detailCalls[mcpID] } func (s *evolvingRuntimeMarketServer) MCPCalls(remotePath, method string) int { s.mu.RLock() defer s.mu.RUnlock() return s.stats.mcpCalls[remotePath][method] } func (s *evolvingRuntimeMarketServer) snapshotFixture(t *testing.T) mockmcp.Fixture { t.Helper() s.mu.RLock() defer s.mu.RUnlock() return cloneFixture(t, s.fixture) } func (s *evolvingRuntimeMarketServer) serveMarketServers(w http.ResponseWriter, r *http.Request) { fixture := s.snapshotFixture(s.t) s.mu.Lock() s.stats.registryCalls++ s.mu.Unlock() response := market.ListResponse{} for _, server := range fixture.Servers { response.Servers = append(response.Servers, market.ServerEnvelope{ Server: market.RegistryServer{ SchemaURI: server.SchemaURI, Name: server.Name, Description: server.Description, Remotes: []market.RegistryRemote{ {Type: "streamable-http", URL: s.URL() + server.RemotePath}, }, }, Meta: market.EnvelopeMeta{ Registry: server.RegistryWithDetailURL(s.URL() + "/mcp/market/detail?mcpId=" + strconv.Itoa(server.Registry.MCPID)), CLI: server.CLI, }, }) } response.Metadata.Count = len(response.Servers) _ = json.NewEncoder(w).Encode(response) } func (s *evolvingRuntimeMarketServer) serveMarketDetail(w http.ResponseWriter, r *http.Request) { fixture := s.snapshotFixture(s.t) mcpID, err := strconv.Atoi(strings.TrimSpace(r.URL.Query().Get("mcpId"))) if err != nil { http.Error(w, "invalid mcpId", http.StatusBadRequest) return } server, ok := fixture.ServerByMCPID(mcpID) if !ok || server.Detail == nil { http.Error(w, "detail fixture missing", http.StatusNotFound) return } s.mu.Lock() if s.stats.detailCalls == nil { s.stats.detailCalls = make(map[int]int) } s.stats.detailCalls[mcpID]++ s.mu.Unlock() _ = json.NewEncoder(w).Encode(server.Detail.Response) } func (s *evolvingRuntimeMarketServer) serveMCP(w http.ResponseWriter, r *http.Request) { fixture := s.snapshotFixture(s.t) server, ok := fixture.ServerByRemotePath(r.URL.Path) if !ok { http.NotFound(w, r) return } if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } var req map[string]any if err := json.NewDecoder(r.Body).Decode(&req); err != nil { http.Error(w, "invalid json-rpc request", http.StatusBadRequest) return } method, _ := req["method"].(string) s.mu.Lock() if s.stats.mcpCalls == nil { s.stats.mcpCalls = make(map[string]map[string]int) } if s.stats.mcpCalls[r.URL.Path] == nil { s.stats.mcpCalls[r.URL.Path] = make(map[string]int) } s.stats.mcpCalls[r.URL.Path][method]++ s.mu.Unlock() switch method { case "initialize": protocolVersion := mockmcp.DefaultProtocolVer capabilities := map[string]any{"tools": map[string]any{"listChanged": false}} serverInfo := map[string]any{"name": server.Name, "version": "0.0.0"} if server.MCP != nil { if server.MCP.ProtocolVersion != "" { protocolVersion = server.MCP.ProtocolVersion } if len(server.MCP.Capabilities) > 0 { capabilities = server.MCP.Capabilities } if len(server.MCP.ServerInfo) > 0 { serverInfo = server.MCP.ServerInfo } } writeJSONRPC(w, req, map[string]any{ "protocolVersion": protocolVersion, "capabilities": capabilities, "serverInfo": serverInfo, }, nil) case "notifications/initialized": w.WriteHeader(http.StatusNoContent) case "tools/list": tools := []any(nil) if server.MCP != nil && len(server.MCP.Tools) > 0 { tools = make([]any, 0, len(server.MCP.Tools)) for _, tool := range server.MCP.Tools { tools = append(tools, tool) } } writeJSONRPC(w, req, map[string]any{"tools": tools}, nil) case "tools/call": toolName := "" if params, ok := req["params"].(map[string]any); ok { if name, ok := params["name"].(string); ok { toolName = name } } call := mockmcp.ToolCallFixture{} if server.MCP != nil && server.MCP.Calls != nil { if fixtureCall, ok := server.MCP.Calls[toolName]; ok { call = fixtureCall } } if call.Error != nil { writeJSONRPC(w, req, nil, call.Error) return } if call.Status != 0 { w.WriteHeader(call.Status) } writeJSONRPC(w, req, call.ResultOrDefault(toolName), nil) default: http.Error(w, "unexpected json-rpc method", http.StatusBadRequest) } } func cloneFixture(t *testing.T, fixture mockmcp.Fixture) mockmcp.Fixture { t.Helper() data, err := json.Marshal(fixture) if err != nil { t.Fatalf("json.Marshal(fixture) error = %v", err) } var out mockmcp.Fixture if err := json.Unmarshal(data, &out); err != nil { t.Fatalf("json.Unmarshal(fixture) error = %v", err) } return out } func protocolFixturePhase1(t *testing.T) mockmcp.Fixture { t.Helper() fixture := mockmcp.DefaultFixture() fixture.Servers = []mockmcp.ServerFixture{fixture.Servers[0]} return fixture } func protocolFixturePhase2(t *testing.T) mockmcp.Fixture { t.Helper() fixture := protocolFixturePhase1(t) doc := &fixture.Servers[0] doc.Registry.UpdatedAt = "2026-03-24T09:00:00Z" doc.CLI.Tools = append(doc.CLI.Tools, market.CLITool{ Name: "archive_document", CLIName: "archive-document", Title: "归档文档", Description: "归档指定文档", IsSensitive: false, Category: "写入", Hidden: false, Flags: map[string]market.CLIFlagHint{ "document_id": {Alias: "doc-id"}, }, }) doc.MCP.Tools = append(doc.MCP.Tools, transport.ToolDescriptor{ Name: "archive_document", Title: "归档文档", Description: "归档指定文档", InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ "document_id": map[string]any{"type": "string"}, }, }, }) doc.MCP.Calls["archive_document"] = mockmcp.ToolCallFixture{ Result: map[string]any{ "content": map[string]any{"archived": true}, }, } doc.Detail.Response.Result.Tools = append(doc.Detail.Response.Result.Tools, market.DetailTool{ ToolName: "archive_document", ToolTitle: "归档文档", ToolDesc: "归档指定文档", IsSensitive: false, ToolRequest: mustJSONString(map[string]any{"type": "object", "properties": map[string]any{"document_id": map[string]any{"type": "string"}}}), ToolResponse: mustJSONString(map[string]any{"type": "object"}), ActionVersion: "G-ACT-VER-102", }) fixture.Servers = append(fixture.Servers, driveServerFixture()) return fixture } func protocolFixturePhase3(t *testing.T) mockmcp.Fixture { t.Helper() fixture := protocolFixturePhase2(t) doc := &fixture.Servers[0] doc.Registry.UpdatedAt = "2026-03-25T09:00:00Z" doc.MCP.Tools = filterMCPToolsByName(doc.MCP.Tools, "search_documents") doc.Detail.Response.Result.Tools = filterDetailToolsByName(doc.Detail.Response.Result.Tools, "search_documents") doc.CLI.Tools = filterCLIToolsByName(doc.CLI.Tools, "search_documents") for idx := range doc.MCP.Tools { if doc.MCP.Tools[idx].Name != "create_document" { continue } doc.MCP.Tools[idx].InputSchema = map[string]any{ "type": "object", "required": []any{"title"}, "properties": map[string]any{ "title": map[string]any{"type": "string"}, "folder_id": map[string]any{"type": "string"}, }, } } for idx := range doc.Detail.Response.Result.Tools { if doc.Detail.Response.Result.Tools[idx].ToolName != "create_document" { continue } doc.Detail.Response.Result.Tools[idx].IsSensitive = false doc.Detail.Response.Result.Tools[idx].ToolTitle = "创建文档V2" doc.Detail.Response.Result.Tools[idx].ToolDesc = "创建文档并可指定目录" doc.Detail.Response.Result.Tools[idx].ToolRequest = mustJSONString(map[string]any{ "type": "object", "required": []any{"title"}, "properties": map[string]any{ "title": map[string]any{"type": "string"}, "folder_id": map[string]any{"type": "string"}, }, }) } for idx := range doc.CLI.Tools { if doc.CLI.Tools[idx].Name != "create_document" { continue } doc.CLI.Tools[idx].CLIName = "create-doc-v2" doc.CLI.Tools[idx].Title = "创建文档V2" doc.CLI.Tools[idx].Description = "创建文档并可指定目录" doc.CLI.Tools[idx].IsSensitive = false doc.CLI.Tools[idx].Flags = map[string]market.CLIFlagHint{ "title": {Alias: "name", Shorthand: "n"}, } } fixture.Servers = filterServersByCommand(fixture.Servers, "drive") return fixture } func protocolFixtureDocOnlyUpdated(t *testing.T) mockmcp.Fixture { t.Helper() fixture := protocolFixturePhase2(t) doc := &fixture.Servers[0] doc.Registry.UpdatedAt = "2026-03-25T10:00:00Z" doc.MCP.ServerInfo["version"] = "1.0.1" for idx := range doc.MCP.Tools { if doc.MCP.Tools[idx].Name != "create_document" { continue } doc.MCP.Tools[idx].Description = "create document updated" } for idx := range doc.Detail.Response.Result.Tools { if doc.Detail.Response.Result.Tools[idx].ToolName != "create_document" { continue } doc.Detail.Response.Result.Tools[idx].ActionVersion = "G-ACT-VER-201" } return fixture } func driveServerFixture() mockmcp.ServerFixture { return mockmcp.ServerFixture{ Name: "钉钉钉盘", Description: "钉盘文件管理", SchemaURI: mockmcp.DefaultSchemaURI, RemotePath: "/server/drive", Registry: market.RegistryMetadata{ IsLatest: true, PublishedAt: "2026-03-21T01:00:00Z", UpdatedAt: "2026-03-21T02:00:00Z", Status: "active", MCPID: 9701, Quality: market.QualityMetadata{ HighQuality: true, Official: true, DTBiz: true, }, }, CLI: market.CLIOverlay{ ID: "drive", Command: "drive", Description: "钉盘文件管理", Prefixes: []string{"drive"}, Tools: []market.CLITool{ { Name: "list_files", CLIName: "list-files", Title: "列出文件", Description: "列出钉盘文件", }, }, }, Detail: &mockmcp.DetailFixture{ Response: market.DetailResponse{ Success: true, Result: market.DetailResult{ MCPID: 9701, Name: "钉钉钉盘", Description: "钉盘文件管理", Tools: []market.DetailTool{ { ToolName: "list_files", ToolTitle: "列出文件", ToolDesc: "列出钉盘文件", IsSensitive: false, ToolRequest: mustJSONString(map[string]any{"type": "object", "properties": map[string]any{"space_id": map[string]any{"type": "string"}}}), ToolResponse: mustJSONString(map[string]any{"type": "object"}), ActionVersion: "DRV-ACT-1", }, }, }, }, }, MCP: &mockmcp.MCPFixture{ ProtocolVersion: mockmcp.DefaultProtocolVer, Capabilities: map[string]any{ "tools": map[string]any{"listChanged": false}, }, ServerInfo: map[string]any{ "name": "drive", "version": "1.0.0", }, Tools: []transport.ToolDescriptor{ { Name: "list_files", Title: "列出文件", Description: "列出钉盘文件", InputSchema: map[string]any{ "type": "object", "properties": map[string]any{ "space_id": map[string]any{"type": "string"}, }, }, }, }, Calls: map[string]mockmcp.ToolCallFixture{ "list_files": { Result: map[string]any{ "content": map[string]any{"items": []any{}}, }, }, }, }, } } func filterCLIToolsByName(tools []market.CLITool, name string) []market.CLITool { out := make([]market.CLITool, 0, len(tools)) for _, tool := range tools { if tool.Name == name { continue } out = append(out, tool) } return out } func filterMCPToolsByName(tools []transport.ToolDescriptor, name string) []transport.ToolDescriptor { out := make([]transport.ToolDescriptor, 0, len(tools)) for _, tool := range tools { if tool.Name == name { continue } out = append(out, tool) } return out } func filterDetailToolsByName(tools []market.DetailTool, name string) []market.DetailTool { out := make([]market.DetailTool, 0, len(tools)) for _, tool := range tools { if tool.ToolName == name { continue } out = append(out, tool) } return out } func filterServersByCommand(servers []mockmcp.ServerFixture, command string) []mockmcp.ServerFixture { out := make([]mockmcp.ServerFixture, 0, len(servers)) for _, server := range servers { if strings.TrimSpace(server.CLI.Command) == command { continue } out = append(out, server) } return out } func mcpSurface(t *testing.T) map[string][]string { t.Helper() root := app.NewRootCommand() mcp := findChild(root, "mcp") if mcp == nil { t.Fatal("mcp command not found in root command tree") } return commandSurface(mcp) } func TestCLIDoesNotSynchronouslyRevalidateWhenCacheAges(t *testing.T) { server := newEvolvingRuntimeMarketServer(t, protocolFixturePhase2(t)) defer server.Close() app.SetDiscoveryBaseURL(server.URL()) t.Cleanup(func() { app.SetDiscoveryBaseURL("") }) cacheDir := t.TempDir() t.Setenv("DWS_CACHE_DIR", cacheDir) mustRunRoot(t, []string{"cache", "refresh"}) server.SetFixture(t, protocolFixtureDocOnlyUpdated(t)) server.ResetStats() ageCacheSnapshots(t, cacheDir, time.Now().UTC().Add(-2*time.Hour)) root := app.NewRootCommand() mcp := findChild(root, "mcp") if mcp == nil { t.Fatal("mcp command not found in root command tree") } assertProductToolsFromCommand(t, mcp, "doc", []string{"archive-document", "create-document", "search-documents"}) assertProductToolsFromCommand(t, mcp, "drive", []string{"list-files"}) if got := server.RegistryCalls(); got != 0 { t.Fatalf("registry calls = %d, want 0", got) } if got := server.MCPCalls("/server/doc", "initialize"); got != 0 { t.Fatalf("doc initialize calls = %d, want 0", got) } if got := server.MCPCalls("/server/doc", "tools/list"); got != 0 { t.Fatalf("doc tools/list calls = %d, want 0", got) } if got := server.MCPCalls("/server/drive", "initialize"); got != 0 { t.Fatalf("drive initialize calls = %d, want 0", got) } if got := server.MCPCalls("/server/drive", "tools/list"); got != 0 { t.Fatalf("drive tools/list calls = %d, want 0", got) } if got := server.DetailCalls(9629); got != 0 { t.Fatalf("doc detail calls = %d, want 0", got) } if got := server.DetailCalls(9701); got != 0 { t.Fatalf("drive detail calls = %d, want 0", got) } } func TestCLIDoesNotSynchronouslyRevalidateWhenRegistryTTLExpires(t *testing.T) { server := newEvolvingRuntimeMarketServer(t, protocolFixturePhase2(t)) defer server.Close() app.SetDiscoveryBaseURL(server.URL()) t.Cleanup(func() { app.SetDiscoveryBaseURL("") }) cacheDir := t.TempDir() t.Setenv("DWS_CACHE_DIR", cacheDir) mustRunRoot(t, []string{"cache", "refresh"}) server.ResetStats() ageCacheSnapshots(t, cacheDir, time.Now().UTC().Add(-25*time.Hour)) root := app.NewRootCommand() mcp := findChild(root, "mcp") if mcp == nil { t.Fatal("mcp command not found in root command tree") } assertProductToolsFromCommand(t, mcp, "doc", []string{"archive-document", "create-document", "search-documents"}) assertProductToolsFromCommand(t, mcp, "drive", []string{"list-files"}) if got := server.RegistryCalls(); got != 0 { t.Fatalf("registry calls = %d, want 0", got) } if got := server.MCPCalls("/server/doc", "initialize"); got != 0 { t.Fatalf("doc initialize calls = %d, want 0", got) } if got := server.MCPCalls("/server/doc", "tools/list"); got != 0 { t.Fatalf("doc tools/list calls = %d, want 0", got) } if got := server.DetailCalls(9629); got != 0 { t.Fatalf("doc detail calls = %d, want 0", got) } if got := server.MCPCalls("/server/drive", "initialize"); got != 0 { t.Fatalf("drive initialize calls = %d, want 0", got) } if got := server.MCPCalls("/server/drive", "tools/list"); got != 0 { t.Fatalf("drive tools/list calls = %d, want 0", got) } if got := server.DetailCalls(9701); got != 0 { t.Fatalf("drive detail calls = %d, want 0", got) } } func commandSurface(parent *cobra.Command) map[string][]string { surface := make(map[string][]string) for _, product := range parent.Commands() { if product.Name() == "help" { continue } tools := make([]string, 0) for _, tool := range product.Commands() { if tool.Name() == "help" { continue } tools = append(tools, tool.Name()) } sort.Strings(tools) surface[product.Name()] = tools } return surface } func assertProductTools(t *testing.T, surface map[string][]string, product string, want []string) { t.Helper() got, ok := surface[product] if !ok { t.Fatalf("product %q not found in mcp surface: %#v", product, surface) } sortedWant := append([]string(nil), want...) sort.Strings(sortedWant) if strings.Join(got, ",") != strings.Join(sortedWant, ",") { t.Fatalf("product %q tools = %#v, want %#v", product, got, sortedWant) } } func assertNoProduct(t *testing.T, surface map[string][]string, product string) { t.Helper() if _, ok := surface[product]; ok { t.Fatalf("product %q should not exist, surface=%#v", product, surface) } } func assertProductToolsFromCommand(t *testing.T, parent *cobra.Command, product string, want []string) { t.Helper() assertProductTools(t, commandSurface(parent), product, want) } func assertNoProductFromCommand(t *testing.T, parent *cobra.Command, product string) { t.Helper() assertNoProduct(t, commandSurface(parent), product) } func findChild(parent *cobra.Command, name string) *cobra.Command { for _, child := range parent.Commands() { if child.Name() == name { return child } } return nil } func mustRunRoot(t *testing.T, args []string) string { t.Helper() out, err := runRoot(t, args) if err != nil { t.Fatalf("Execute(%v) error = %v\noutput:\n%s", args, err, out) } return out } func mustRunRootJSON(t *testing.T, args []string) map[string]any { t.Helper() out := mustRunRoot(t, args) var payload map[string]any if err := json.Unmarshal([]byte(out), &payload); err != nil { t.Fatalf("json.Unmarshal(%v) error = %v\noutput:\n%s", args, err, out) } return payload } func runRoot(t *testing.T, args []string) (string, error) { t.Helper() cmd := app.NewRootCommand() var out bytes.Buffer cmd.SetOut(&out) cmd.SetErr(&out) cmd.SetIn(strings.NewReader("yes\n")) cmd.SetArgs(args) err := cmd.Execute() return out.String(), err } func ageCacheSnapshots(t *testing.T, root string, savedAt time.Time) { t.Helper() walkErr := filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error { if err != nil { return err } if d.IsDir() || !strings.HasSuffix(path, ".json") { return nil } data, err := os.ReadFile(path) if err != nil { return err } var payload map[string]any if err := json.Unmarshal(data, &payload); err != nil { return nil } if _, ok := payload["saved_at"]; !ok { return nil } payload["saved_at"] = savedAt.Format(time.RFC3339Nano) rewritten, err := json.MarshalIndent(payload, "", " ") if err != nil { return err } return os.WriteFile(path, rewritten, 0o644) }) if walkErr != nil { t.Fatalf("ageCacheSnapshots() error = %v", walkErr) } } func mustJSONString(value any) string { data, err := json.Marshal(value) if err != nil { panic(err) } return string(data) }