Files
dingtalk-workspace-cli/internal/cli/loader_supplement_test.go
T
xianfeng wangandgithub-actions[bot] 7692048cf4 fix(discovery): surface edition supplement/fallback servers in runtime catalog loader (#197)
* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* chore: update coverage badge [skip ci]

* fix conference discovery

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-04-30 09:44:25 +08:00

152 lines
5.2 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 cli
import (
"testing"
"time"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cache"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/market"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
// TestLoadFromCache_SupplementFillsGaps simulates the Wukong gray-release
// scenario: the Portal envelope only carries `live`, but the edition's
// SupplementServers hook ships the hardcoded endpoints for `conference` and
// `todo`. The resulting catalog must expose all three so runtime endpoint
// resolution does not depend on the historical default-partition accident.
func TestLoadFromCache_SupplementFillsGaps(t *testing.T) {
setEdition(t, &edition.Hooks{
Name: "wukong",
SupplementServers: func() []edition.ServerInfo {
return []edition.ServerInfo{
{ID: "conference", Name: "会议", Endpoint: "https://example.invalid/conference"},
{ID: "todo", Name: "待办", Endpoint: "https://example.invalid/todo"},
// Duplicate of the discovery entry — MUST be overridden by
// the discovery entry (discovery wins on ID collision).
{ID: "live", Name: "直播(supplement)", Endpoint: "https://example.invalid/overridden"},
}
},
})
root := t.TempDir()
now := time.Date(2026, 4, 28, 0, 0, 0, 0, time.UTC)
store := cache.NewStore(root)
store.Now = func() time.Time { return now }
liveEndpoint := "https://example.invalid/live"
seedRegistryCache(t, store, "wukong/default", now, []market.ServerDescriptor{
{
Key: "live",
DisplayName: "直播",
Endpoint: liveEndpoint,
CLI: market.CLIOverlay{
ID: "live",
Command: "live",
},
},
})
loader := EnvironmentLoader{}
state := loader.loadFromCache(store)
if !state.Available {
t.Fatalf("expected cached state available; got %+v", state)
}
wantIDs := map[string]string{
"conference": "https://example.invalid/conference",
"todo": "https://example.invalid/todo",
"live": liveEndpoint, // discovery wins, NOT the supplement's overridden URL
}
for id, wantEndpoint := range wantIDs {
product, ok := state.Catalog.FindProduct(id)
if !ok {
t.Errorf("catalog missing product %q; have %v", id, productIDs(state.Catalog.Products))
continue
}
if product.Endpoint != wantEndpoint {
t.Errorf("product %q endpoint = %q, want %q", id, product.Endpoint, wantEndpoint)
}
}
}
// TestLoadFromCache_EmptyRegistry_StillExposesSupplement covers the cold-start
// gray-release case: no cached registry at all, but the edition still knows
// about a set of hardcoded products. Those should be exposed via the catalog
// so `dws foo bar` does not fail with endpoint_not_resolved on first run.
func TestLoadFromCache_EmptyRegistry_StillExposesSupplement(t *testing.T) {
setEdition(t, &edition.Hooks{
Name: "wukong",
SupplementServers: func() []edition.ServerInfo {
return []edition.ServerInfo{
{ID: "conference", Name: "会议", Endpoint: "https://example.invalid/conference"},
}
},
})
root := t.TempDir()
now := time.Date(2026, 4, 28, 0, 0, 0, 0, time.UTC)
store := cache.NewStore(root)
store.Now = func() time.Time { return now }
loader := EnvironmentLoader{}
state := loader.loadFromCache(store)
if !state.Available {
t.Fatalf("expected cached state available via supplement; got %+v", state)
}
if _, ok := state.Catalog.FindProduct("conference"); !ok {
t.Fatalf("supplement did not surface conference into catalog; products=%v", productIDs(state.Catalog.Products))
}
if !state.NeedsRevalidate {
t.Errorf("NeedsRevalidate should be true when only supplement is available")
}
}
// TestFallbackRuntimeServers_UsedWhenDiscoveryFailsWithoutCache exercises
// the worst-case path: no cached registry AND no live discovery (embedded
// scenario where AuthTokenFunc returns empty). FallbackServers must still
// surface a usable catalog.
func TestFallbackRuntimeServers_UsedWhenDiscoveryFailsWithoutCache(t *testing.T) {
setEdition(t, &edition.Hooks{
Name: "wukong",
FallbackServers: func() []edition.ServerInfo {
return []edition.ServerInfo{
{ID: "conference", Name: "会议", Endpoint: "https://fallback.invalid/conference"},
}
},
SupplementServers: func() []edition.ServerInfo {
return []edition.ServerInfo{
{ID: "extra", Name: "Extra", Endpoint: "https://fallback.invalid/extra"},
}
},
})
rs := fallbackRuntimeServers()
if len(rs) != 2 {
t.Fatalf("fallbackRuntimeServers() len = %d, want 2 (fallback + non-overlapping supplement); got %v", len(rs), rs)
}
ids := make(map[string]bool, len(rs))
for _, r := range rs {
ids[r.Server.CLI.ID] = true
}
for _, want := range []string{"conference", "extra"} {
if !ids[want] {
t.Errorf("fallbackRuntimeServers() missing %q; have %v", want, ids)
}
}
}