Files
dingtalk-workspace-cli/internal/interfacesnapshot/compare_test.go

368 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 interfacesnapshot
import "testing"
func TestCrossPlatformCoverageCompareAdmissionPolicy(t *testing.T) {
base := testSnapshot(
testCommand("dws"),
testCommand("dws search", testFlag("query", "string", false)),
)
tests := []struct {
name string
current Snapshot
compatible bool
kind string
}{
{
name: "command addition allowed",
current: testSnapshot(
testCommand("dws"),
testCommand("dws search", testFlag("query", "string", false)),
testCommand("dws status"),
),
compatible: true,
},
{
name: "flag addition allowed",
current: testSnapshot(
testCommand("dws"),
testCommand("dws search",
testFlag("limit", "int", false),
testFlag("query", "string", false),
),
),
compatible: true,
},
{
name: "required flag addition blocked on existing command",
current: testSnapshot(
testCommand("dws"),
testCommand("dws search",
testFlag("query", "string", false),
testFlag("tenant", "string", true),
),
),
kind: "required_flag_added",
},
{
name: "command deletion blocked",
current: testSnapshot(
testCommand("dws"),
),
kind: "command_removed",
},
{
name: "rename without alias blocked",
current: testSnapshot(
testCommand("dws"),
testCommand("dws find", testFlag("query", "string", false)),
),
kind: "command_removed",
},
{
name: "flag deletion blocked",
current: testSnapshot(
testCommand("dws"),
testCommand("dws search"),
),
kind: "flag_removed",
},
{
name: "optional becoming required blocked",
current: testSnapshot(
testCommand("dws"),
testCommand("dws search", testFlag("query", "string", true)),
),
kind: "flag_became_required",
},
{
name: "flag type change blocked",
current: testSnapshot(
testCommand("dws"),
testCommand("dws search", testFlag("query", "stringSlice", false)),
),
kind: "flag_type_changed",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
comparison := Compare(test.current, base, "base")
if comparison.Compatible != test.compatible {
t.Fatalf("Compatible = %v, want %v; blocking=%#v", comparison.Compatible, test.compatible, comparison.Blocking)
}
if test.kind == "" {
return
}
if !hasChangeKind(comparison.Blocking, test.kind) {
t.Fatalf("blocking=%#v, want kind %q", comparison.Blocking, test.kind)
}
})
}
}
func TestCrossPlatformCoverageCompareAllowsRenameWhenOldPathIsAlias(t *testing.T) {
base := testSnapshot(
testCommand("dws"),
testCommand("dws search", testFlag("query", "string", false)),
)
current := testSnapshot(
testCommand("dws"),
testCommandWithAliases("dws find", []string{"search"}, testFlag("query", "string", false)),
)
comparison := Compare(current, base, "base")
if !comparison.Compatible {
t.Fatalf("rename with compatibility alias was blocked: %#v", comparison.Blocking)
}
}
func TestCrossPlatformCoverageCompareBlocksRemovedAlias(t *testing.T) {
base := testSnapshot(
testCommand("dws"),
testCommandWithAliases("dws search", []string{"find"}),
)
current := testSnapshot(
testCommand("dws"),
testCommand("dws search"),
)
comparison := Compare(current, base, "base")
if comparison.Compatible || !hasChangeKind(comparison.Blocking, "command_alias_removed") {
t.Fatalf("removed alias was not blocked: %#v", comparison)
}
}
func TestCrossPlatformCoverageCompareBlocksAliasRetargetedToIncompatibleCommand(t *testing.T) {
base := testSnapshot(
testCommand("dws"),
testCommandWithAliases("dws search", []string{"find"}, testFlag("query", "string", false)),
)
current := testSnapshot(
testCommand("dws"),
testCommand("dws search", testFlag("query", "string", false)),
testCommand("dws find"),
)
comparison := Compare(current, base, "base")
if comparison.Compatible || !hasFlagChange(comparison.Blocking, "flag_removed", "dws find", "query") {
t.Fatalf("alias retarget was not checked against its old contract: %#v", comparison)
}
}
func TestCrossPlatformCoverageCompareBlocksSnapshotRuleChanges(t *testing.T) {
base := testSnapshot(testCommand("dws"))
current := testSnapshot(testCommand("dws"))
current.Rules.ExcludedFlags = append(current.Rules.ExcludedFlags, "legacy")
comparison := Compare(current, base, "base")
if comparison.Compatible || !hasChangeKind(comparison.Blocking, "snapshot_rules_changed") {
t.Fatalf("snapshot rule change was not blocked: %#v", comparison)
}
}
func TestCrossPlatformCoverageCompareBlocksCallableMetadataRegressions(t *testing.T) {
baseFlag := testFlag("format", "string", false)
baseFlag.Shorthand = "f"
baseFlag.NoOpt = "json"
base := testSnapshot(
testCommand("dws"),
testCommand("dws export", baseFlag),
)
tests := []struct {
name string
command Command
kind string
}{
{
name: "command became non-runnable",
command: func() Command {
command := testCommand("dws export", baseFlag)
command.Runnable = false
return command
}(),
kind: "command_became_non_runnable",
},
{
name: "command became hidden",
command: func() Command {
command := testCommand("dws export", baseFlag)
command.Hidden = true
return command
}(),
kind: "command_became_hidden",
},
{
name: "flag shorthand removed",
command: func() Command {
flag := baseFlag
flag.Shorthand = ""
return testCommand("dws export", flag)
}(),
kind: "flag_shorthand_changed",
},
{
name: "flag no-opt behavior removed",
command: func() Command {
flag := baseFlag
flag.NoOpt = ""
return testCommand("dws export", flag)
}(),
kind: "flag_no_opt_changed",
},
{
name: "flag became hidden",
command: func() Command {
flag := baseFlag
flag.Hidden = true
return testCommand("dws export", flag)
}(),
kind: "flag_became_hidden",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
current := testSnapshot(testCommand("dws"), test.command)
comparison := Compare(current, base, "base")
if comparison.Compatible || !hasChangeKind(comparison.Blocking, test.kind) {
t.Fatalf("metadata regression %q was not blocked: %#v", test.kind, comparison)
}
})
}
}
func TestCrossPlatformCoverageCompareUsesEffectiveFlagsAtEveryCommandPath(t *testing.T) {
base := testSnapshot(
testCommand("dws", testFlag("profile", "string", false)),
testCommandWithFlagScopes(
"dws search",
[]Flag{testFlag("query", "string", false)},
[]Flag{testFlag("profile", "string", false)},
),
)
t.Run("persistent flag scope narrowing is blocked", func(t *testing.T) {
current := testSnapshot(
testCommand("dws", testFlag("profile", "string", false)),
testCommand("dws search", testFlag("query", "string", false)),
)
comparison := Compare(current, base, "base")
if comparison.Compatible || !hasFlagChange(comparison.Blocking, "flag_removed", "dws search", "profile") {
t.Fatalf("lost inherited flag was not blocked: %#v", comparison)
}
})
t.Run("local flag moved to inherited remains compatible", func(t *testing.T) {
current := testSnapshot(
testCommand("dws",
testFlag("profile", "string", false),
testFlag("query", "string", false),
),
testCommandWithFlagScopes(
"dws search",
nil,
[]Flag{
testFlag("profile", "string", false),
testFlag("query", "string", false),
},
),
)
comparison := Compare(current, base, "base")
if !comparison.Compatible {
t.Fatalf("flag moved to an inherited scope was blocked: %#v", comparison.Blocking)
}
})
}
func TestCrossPlatformCoverageCompareAllRequiresBothReferencesToPass(t *testing.T) {
current := testSnapshot(testCommand("dws"), testCommand("dws status"))
mergeBase := testSnapshot(testCommand("dws"))
stable := testSnapshot(testCommand("dws"), testCommand("dws legacy"))
report := CompareAll(current, map[string]Snapshot{
"stable": stable,
"merge-base": mergeBase,
})
if report.Compatible {
t.Fatal("aggregate report passed even though stable comparison removed a command")
}
if len(report.Comparisons) != 2 || report.Comparisons[0].Reference != "merge-base" || report.Comparisons[1].Reference != "stable" {
t.Fatalf("comparisons are not deterministic: %#v", report.Comparisons)
}
if !report.Comparisons[0].Compatible || report.Comparisons[1].Compatible {
t.Fatalf("unexpected per-reference results: %#v", report.Comparisons)
}
}
func testSnapshot(commands ...Command) Snapshot {
return Snapshot{
SchemaVersion: SchemaVersion,
Rules: Rules{
ExcludedCommandSubtrees: append([]string(nil), excludedCommandSubtrees...),
ExcludedFlags: []string{"help"},
},
Commands: commands,
}
}
func testCommand(path string, flags ...Flag) Command {
return testCommandWithAliases(path, nil, flags...)
}
func testCommandWithAliases(path string, aliases []string, flags ...Flag) Command {
return Command{
Path: path,
Runnable: true,
Aliases: aliases,
LocalFlags: flags,
InheritedFlags: []Flag{},
}
}
func testCommandWithFlagScopes(path string, local, inherited []Flag) Command {
return Command{
Path: path,
Aliases: []string{},
LocalFlags: local,
InheritedFlags: inherited,
}
}
func testFlag(name, flagType string, required bool) Flag {
return Flag{Name: name, Type: flagType, Required: required}
}
func hasChangeKind(changes []Change, kind string) bool {
for _, change := range changes {
if change.Kind == kind {
return true
}
}
return false
}
func hasFlagChange(changes []Change, kind, path, flag string) bool {
for _, change := range changes {
if change.Kind == kind && change.Path == path && change.Flag == flag {
return true
}
}
return false
}