1054 lines
32 KiB
Go
1054 lines
32 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 (
|
|
"os"
|
|
"regexp"
|
|
"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 TestCrossPlatformCoverageCompareAllowsBoolConstParamsBootstrapAddition(t *testing.T) {
|
|
base := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws send"),
|
|
)
|
|
current := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws send"),
|
|
)
|
|
current.Commands[1].BoolConstParams = map[string]bool{"convThreadEnabled": true}
|
|
|
|
comparison := Compare(current, base, "base")
|
|
if !comparison.Compatible || len(comparison.Blocking) != 0 || len(comparison.Additions) != 0 {
|
|
t.Fatalf("ordinary Compare treated bootstrapped bool ConstParams evidence as a compatibility change: %#v", comparison)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareBlocksBoolConstParamsDriftAfterBootstrap(t *testing.T) {
|
|
base := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws send"),
|
|
)
|
|
base.Commands[1].BoolConstParams = map[string]bool{"convThreadEnabled": true}
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
current map[string]bool
|
|
}{
|
|
{name: "removed", current: nil},
|
|
{name: "value flipped", current: map[string]bool{"convThreadEnabled": false}},
|
|
{name: "extra key", current: map[string]bool{"convThreadEnabled": true, "other": false}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
current := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws send"),
|
|
)
|
|
current.Commands[1].BoolConstParams = test.current
|
|
|
|
comparison := Compare(current, base, "base")
|
|
if comparison.Compatible {
|
|
t.Fatalf("historical bool ConstParams drift was accepted: %#v", comparison)
|
|
}
|
|
if len(comparison.Blocking) != 1 {
|
|
t.Fatalf("blocking=%#v, want exactly one durable contract change", comparison.Blocking)
|
|
}
|
|
change := comparison.Blocking[0]
|
|
if change.Kind != "bool_const_params_changed" || change.Path != "dws send" {
|
|
t.Fatalf("blocking=%#v, want bool_const_params_changed at dws send", comparison.Blocking)
|
|
}
|
|
if len(comparison.Additions) != 0 {
|
|
t.Fatalf("bool ConstParams drift also produced additions: %#v", comparison.Additions)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
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 TestCrossPlatformCoverageCompareBlocksFlagAliasTargetRegression(t *testing.T) {
|
|
baseLegacy := testFlag("legacy-id", "string", false)
|
|
baseLegacy.AliasOf = "message-id"
|
|
base := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws send", baseLegacy, testFlag("message-id", "string", false)),
|
|
)
|
|
|
|
for _, target := range []string{"", "other-id"} {
|
|
t.Run("target_"+target, func(t *testing.T) {
|
|
currentLegacy := baseLegacy
|
|
currentLegacy.AliasOf = target
|
|
current := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand(
|
|
"dws send",
|
|
currentLegacy,
|
|
testFlag("message-id", "string", false),
|
|
testFlag("other-id", "string", false),
|
|
),
|
|
)
|
|
comparison := Compare(current, base, "base")
|
|
if comparison.Compatible || !hasFlagChange(
|
|
comparison.Blocking,
|
|
"flag_alias_target_changed",
|
|
"dws send",
|
|
"legacy-id",
|
|
) {
|
|
t.Fatalf("alias target regression was not blocked: %#v", 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 TestCrossPlatformCoverageCompareAllWithFlagMigrationsAuthorizesOnlyTheExactTransition(t *testing.T) {
|
|
before := testFlagMigrationSnapshot(false, true)
|
|
authority := testFlagMigrationManifest(FlagMigrationPending)
|
|
candidate := testFlagMigrationManifest(FlagMigrationConsumed)
|
|
|
|
report, err := CompareAllWithFlagMigrations(
|
|
testFlagMigrationSnapshot(true, true),
|
|
map[string]Snapshot{
|
|
"merge-base": before,
|
|
"stable": before,
|
|
},
|
|
authority,
|
|
candidate,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("exact base-owned migration was rejected: %v", err)
|
|
}
|
|
if !report.Compatible {
|
|
t.Fatalf("exact migration remained incompatible: %#v", report.Comparisons)
|
|
}
|
|
for _, comparison := range report.Comparisons {
|
|
if len(comparison.Blocking) != 0 {
|
|
t.Fatalf("reference %q retained blocking changes: %#v", comparison.Reference, comparison.Blocking)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsComposesWithReviewedTypeChange(t *testing.T) {
|
|
registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"})
|
|
|
|
before := testFlagMigrationSnapshot(false, true)
|
|
before.Commands = append(before.Commands, testCommand("dws search", testFlag("query", "string", false)))
|
|
after := testFlagMigrationSnapshot(true, true)
|
|
after.Commands = append(after.Commands, testCommand("dws search", testFlag("query", "int", false)))
|
|
|
|
report, err := CompareAllWithFlagMigrations(
|
|
after,
|
|
map[string]Snapshot{
|
|
"merge-base": before,
|
|
"stable": before,
|
|
},
|
|
testFlagMigrationManifest(FlagMigrationPending),
|
|
testFlagMigrationManifest(FlagMigrationConsumed),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("composed reviewed migrations were rejected: %v", err)
|
|
}
|
|
if !report.Compatible {
|
|
t.Fatalf("composed reviewed migrations remained incompatible: %#v", report.Comparisons)
|
|
}
|
|
}
|
|
|
|
// registerReviewedFixture puts a fixture command in the reviewed table for the
|
|
// duration of one test, so the behaviour tests exercise the real lookup instead
|
|
// of depending on whichever production entries happen to exist.
|
|
func registerReviewedFixture(t *testing.T, change flagTypeChange) {
|
|
t.Helper()
|
|
if _, exists := reviewedFlagTypeChanges[change]; exists {
|
|
t.Fatalf("fixture %+v collides with a production entry", change)
|
|
}
|
|
reviewedFlagTypeChanges[change] = struct{}{}
|
|
t.Cleanup(func() { delete(reviewedFlagTypeChanges, change) })
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareAcceptsReviewedFlagTypeChange(t *testing.T) {
|
|
base := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws search", testFlag("query", "string", false)),
|
|
)
|
|
current := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws search", testFlag("query", "int", false)),
|
|
)
|
|
|
|
// Without an entry the migration is still a blocking change.
|
|
if comparison := Compare(current, base, "base"); !hasChangeKind(comparison.Blocking, "flag_type_changed") {
|
|
t.Fatalf("an unreviewed type change should block: %#v", comparison.Blocking)
|
|
}
|
|
|
|
registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"})
|
|
if comparison := Compare(current, base, "base"); !comparison.Compatible {
|
|
t.Fatalf("a reviewed string->int migration should pass: %#v", comparison.Blocking)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareReviewedFlagTypeChangeIsDirectionSensitive(t *testing.T) {
|
|
registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"})
|
|
|
|
// The reverse migration shares command and flag but not direction, so the
|
|
// string->int entry must not admit it.
|
|
base := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws search", testFlag("query", "int", false)),
|
|
)
|
|
current := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws search", testFlag("query", "string", false)),
|
|
)
|
|
if comparison := Compare(current, base, "base"); comparison.Compatible {
|
|
t.Fatal("int->string must not be admitted by a string->int entry")
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareReviewedFlagTypeChangeRejectsOtherCommandsAndFlags(t *testing.T) {
|
|
base := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws search", testFlag("query", "string", false)),
|
|
)
|
|
current := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws search", testFlag("query", "int", false)),
|
|
)
|
|
|
|
// Same flag and direction, different command.
|
|
registerReviewedFixture(t, flagTypeChange{CommandPath: "dws status", Flag: "query", From: "string", To: "int"})
|
|
if comparison := Compare(current, base, "base"); comparison.Compatible {
|
|
t.Fatal("an entry for another command must not admit this migration")
|
|
}
|
|
|
|
// Same command and direction, different flag.
|
|
registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "elsewhere", From: "string", To: "int"})
|
|
if comparison := Compare(current, base, "base"); comparison.Compatible {
|
|
t.Fatal("an entry for another flag must not admit this migration")
|
|
}
|
|
}
|
|
|
|
// A reviewed migration is only accepted when the rest of the flag's contract
|
|
// held still. Each case bundles one unrelated regression with the reviewed type
|
|
// change and requires the type failure to reappear, which also anchors
|
|
// flagContractOtherwiseChanged against the checks it mirrors: a condition that
|
|
// drifts out of sync fails here instead of silently widening the exemption.
|
|
func TestCrossPlatformCoverageCompareReviewedFlagTypeChangeRejectsBundledRegression(t *testing.T) {
|
|
registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"})
|
|
|
|
tests := []struct {
|
|
name string
|
|
base Flag
|
|
next Flag
|
|
kind string
|
|
}{
|
|
{
|
|
name: "became required",
|
|
base: Flag{Name: "query", Type: "string"},
|
|
next: Flag{Name: "query", Type: "int", Required: true},
|
|
kind: "flag_became_required",
|
|
},
|
|
{
|
|
name: "lost shorthand",
|
|
base: Flag{Name: "query", Type: "string", Shorthand: "q"},
|
|
next: Flag{Name: "query", Type: "int"},
|
|
kind: "flag_shorthand_changed",
|
|
},
|
|
{
|
|
name: "changed no-opt",
|
|
base: Flag{Name: "query", Type: "string", NoOpt: "all"},
|
|
next: Flag{Name: "query", Type: "int", NoOpt: "1"},
|
|
kind: "flag_no_opt_changed",
|
|
},
|
|
{
|
|
name: "became hidden",
|
|
base: Flag{Name: "query", Type: "string"},
|
|
next: Flag{Name: "query", Type: "int", Hidden: true},
|
|
kind: "flag_became_hidden",
|
|
},
|
|
{
|
|
name: "changed alias target",
|
|
base: Flag{Name: "query", Type: "string", AliasOf: "old-query"},
|
|
next: Flag{Name: "query", Type: "int", AliasOf: "new-query"},
|
|
kind: "flag_alias_target_changed",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
base := testSnapshot(testCommand("dws"), testCommand("dws search", test.base))
|
|
current := testSnapshot(testCommand("dws"), testCommand("dws search", test.next))
|
|
comparison := Compare(current, base, "base")
|
|
if !hasChangeKind(comparison.Blocking, test.kind) {
|
|
t.Fatalf("blocking=%#v, want kind %q", comparison.Blocking, test.kind)
|
|
}
|
|
if !hasChangeKind(comparison.Blocking, "flag_type_changed") {
|
|
t.Fatalf("a bundled regression must re-report the type change: %#v", comparison.Blocking)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// The exemption is resolved against the canonical Command.Path, never the
|
|
// alias-expanded accepted path. An aliased command enters compareEffectiveFlags
|
|
// once per accepted spelling, so keying on the accepted path would let every
|
|
// alias spelling re-report a reviewed migration.
|
|
func TestCrossPlatformCoverageCompareResolvesReviewedFlagTypeChangeByCanonicalPath(t *testing.T) {
|
|
base := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommandWithAliases("dws search", []string{"find"}, testFlag("query", "string", false)),
|
|
)
|
|
current := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommandWithAliases("dws search", []string{"find"}, testFlag("query", "int", false)),
|
|
)
|
|
|
|
// The canonical path admits the migration for every accepted spelling.
|
|
canonical := flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"}
|
|
registerReviewedFixture(t, canonical)
|
|
if comparison := Compare(current, base, "base"); !comparison.Compatible {
|
|
t.Fatalf("canonical path should admit the migration on every alias: %#v", comparison.Blocking)
|
|
}
|
|
delete(reviewedFlagTypeChanges, canonical)
|
|
|
|
// An entry spelled with the alias path matches nothing, because the lookup
|
|
// never sees the accepted spelling.
|
|
registerReviewedFixture(t, flagTypeChange{CommandPath: "dws find", Flag: "query", From: "string", To: "int"})
|
|
if comparison := Compare(current, base, "base"); comparison.Compatible {
|
|
t.Fatal("an alias-spelled entry must not admit the migration")
|
|
}
|
|
}
|
|
|
|
// A string -> int migration always changes the recorded default ("" to "0").
|
|
// Defaults are not part of what this gate enforces, and folding them into
|
|
// flagContractOtherwiseChanged would make every table entry dead on arrival.
|
|
func TestCrossPlatformCoverageCompareReviewedFlagTypeChangeToleratesDefaultDrift(t *testing.T) {
|
|
registerReviewedFixture(t, flagTypeChange{CommandPath: "dws search", Flag: "query", From: "string", To: "int"})
|
|
|
|
base := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws search", Flag{Name: "query", Type: "string", Default: ""}),
|
|
)
|
|
current := testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws search", Flag{Name: "query", Type: "int", Default: "0"}),
|
|
)
|
|
if comparison := Compare(current, base, "base"); !comparison.Compatible {
|
|
t.Fatalf("default drift must not defeat a reviewed migration: %#v", comparison.Blocking)
|
|
}
|
|
}
|
|
|
|
// reviewedEntryPattern matches one table entry as gofmt writes it on a single
|
|
// line. It is used to read the sibling copy of the table out of the policy
|
|
// helper, which cannot be imported: that directory is copied into a worktree at
|
|
// a historical revision and built there, so it must not depend on this package.
|
|
var reviewedEntryPattern = regexp.MustCompile(
|
|
`\{CommandPath:\s*"([^"]*)",\s*Flag:\s*"([^"]*)",\s*From:\s*"([^"]*)",\s*To:\s*"([^"]*)"\}`)
|
|
|
|
const interfaceBaselineReviewedPath = "../../scripts/policy/interface-baseline/reviewed.go"
|
|
|
|
// The modern authority and legacy smoke helper keep mirrored tables so local
|
|
// smoke checks cannot disagree with the authoritative comparison.
|
|
func TestCrossPlatformCoverageReviewedFlagTypeTableMatchesInterfaceBaseline(t *testing.T) {
|
|
source, err := os.ReadFile(interfaceBaselineReviewedPath)
|
|
if err != nil {
|
|
t.Fatalf("读取 %s 失败: %v", interfaceBaselineReviewedPath, err)
|
|
}
|
|
|
|
sibling := make(map[flagTypeChange]struct{})
|
|
for _, match := range reviewedEntryPattern.FindAllStringSubmatch(string(source), -1) {
|
|
sibling[flagTypeChange{CommandPath: match[1], Flag: match[2], From: match[3], To: match[4]}] = struct{}{}
|
|
}
|
|
|
|
// Without this the whole guard degrades into comparing two empty sets the
|
|
// moment the pattern stops matching, which is exactly how a duplicated
|
|
// allowlist goes stale unnoticed.
|
|
if len(sibling) == 0 {
|
|
t.Fatalf("未能从 %s 提取到任何条目:正则或表的书写形态已漂移,这条守卫已失效", interfaceBaselineReviewedPath)
|
|
}
|
|
|
|
for change := range reviewedFlagTypeChanges {
|
|
if _, ok := sibling[change]; !ok {
|
|
t.Errorf("%+v 只存在于 interfacesnapshot,缺在 %s", change, interfaceBaselineReviewedPath)
|
|
}
|
|
}
|
|
for change := range sibling {
|
|
if _, ok := reviewedFlagTypeChanges[change]; !ok {
|
|
t.Errorf("%+v 只存在于 %s,缺在 interfacesnapshot", change, interfaceBaselineReviewedPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsRejectsInvalidLifecycleTransitions(t *testing.T) {
|
|
empty := testEmptyFlagMigrationManifest()
|
|
pending := testFlagMigrationManifest(FlagMigrationPending)
|
|
consumed := testFlagMigrationManifest(FlagMigrationConsumed)
|
|
before := testFlagMigrationSnapshot(false, true)
|
|
after := testFlagMigrationSnapshot(true, true)
|
|
|
|
partial := testFlagMigrationSnapshot(false, true)
|
|
partial.Commands[1].LocalFlags[0].Hidden = true
|
|
partial.Commands[1].LocalFlags[0].Required = false
|
|
partial.Commands[1].LocalFlags[0].AliasOf = "message-id"
|
|
|
|
typeDrift := testFlagMigrationSnapshot(true, true)
|
|
typeDrift.Commands[1].LocalFlags[0].Type = "stringSlice"
|
|
noOptDrift := testFlagMigrationSnapshot(true, true)
|
|
noOptDrift.Commands[1].LocalFlags[0].NoOpt = ""
|
|
shorthandDrift := testFlagMigrationSnapshot(true, true)
|
|
shorthandDrift.Commands[1].LocalFlags[0].Shorthand = "x"
|
|
|
|
tests := []struct {
|
|
name string
|
|
current Snapshot
|
|
authority FlagMigrationManifest
|
|
candidate FlagMigrationManifest
|
|
}{
|
|
{
|
|
name: "candidate-added pending record cannot approve its own surface change",
|
|
current: after,
|
|
authority: empty,
|
|
candidate: pending,
|
|
},
|
|
{
|
|
name: "candidate-added consumed record cannot approve its own surface change",
|
|
current: after,
|
|
authority: empty,
|
|
candidate: consumed,
|
|
},
|
|
{
|
|
name: "partial migration is rejected",
|
|
current: partial,
|
|
authority: pending,
|
|
candidate: consumed,
|
|
},
|
|
{
|
|
name: "pending record cannot be falsely consumed before the surface changes",
|
|
current: before,
|
|
authority: pending,
|
|
candidate: consumed,
|
|
},
|
|
{
|
|
name: "completed surface change must consume its pending record",
|
|
current: after,
|
|
authority: pending,
|
|
candidate: pending,
|
|
},
|
|
{
|
|
name: "legacy type drift is outside the approval",
|
|
current: typeDrift,
|
|
authority: pending,
|
|
candidate: consumed,
|
|
},
|
|
{
|
|
name: "legacy no-opt drift is outside the approval",
|
|
current: noOptDrift,
|
|
authority: pending,
|
|
candidate: consumed,
|
|
},
|
|
{
|
|
name: "legacy shorthand drift is outside the approval",
|
|
current: shorthandDrift,
|
|
authority: pending,
|
|
candidate: consumed,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
report, err := CompareAllWithFlagMigrations(
|
|
test.current,
|
|
map[string]Snapshot{
|
|
"merge-base": before,
|
|
"stable": before,
|
|
},
|
|
test.authority,
|
|
test.candidate,
|
|
)
|
|
if err == nil {
|
|
t.Fatalf("invalid transition passed: %#v", report)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsAllowsApprovalCreationBeforeSurfaceChange(t *testing.T) {
|
|
before := testFlagMigrationSnapshot(false, true)
|
|
report, err := CompareAllWithFlagMigrations(
|
|
before,
|
|
map[string]Snapshot{
|
|
"merge-base": before,
|
|
"stable": before,
|
|
},
|
|
testEmptyFlagMigrationManifest(),
|
|
testFlagMigrationManifest(FlagMigrationPending),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("pending approval creation was rejected before the surface changed: %v", err)
|
|
}
|
|
if !report.Compatible {
|
|
t.Fatalf("pending approval creation changed compatibility: %#v", report.Comparisons)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsKeepsUnrelatedBreakageBlocking(t *testing.T) {
|
|
before := testFlagMigrationSnapshot(false, true)
|
|
pending := testFlagMigrationManifest(FlagMigrationPending)
|
|
consumed := testFlagMigrationManifest(FlagMigrationConsumed)
|
|
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*Snapshot)
|
|
kind string
|
|
flag string
|
|
}{
|
|
{
|
|
name: "unrelated flag removal",
|
|
mutate: func(snapshot *Snapshot) {
|
|
snapshot.Commands[1].LocalFlags = snapshot.Commands[1].LocalFlags[:2]
|
|
},
|
|
kind: "flag_removed",
|
|
flag: "format",
|
|
},
|
|
{
|
|
name: "unrelated flag type change",
|
|
mutate: func(snapshot *Snapshot) {
|
|
snapshot.Commands[1].LocalFlags[2].Type = "stringSlice"
|
|
},
|
|
kind: "flag_type_changed",
|
|
flag: "format",
|
|
},
|
|
{
|
|
name: "unrelated flag no-opt change",
|
|
mutate: func(snapshot *Snapshot) {
|
|
snapshot.Commands[1].LocalFlags[2].NoOpt = ""
|
|
},
|
|
kind: "flag_no_opt_changed",
|
|
flag: "format",
|
|
},
|
|
{
|
|
name: "unrelated flag shorthand change",
|
|
mutate: func(snapshot *Snapshot) {
|
|
snapshot.Commands[1].LocalFlags[2].Shorthand = ""
|
|
},
|
|
kind: "flag_shorthand_changed",
|
|
flag: "format",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
current := testFlagMigrationSnapshot(true, true)
|
|
test.mutate(¤t)
|
|
report, err := CompareAllWithFlagMigrations(
|
|
current,
|
|
map[string]Snapshot{
|
|
"merge-base": before,
|
|
"stable": before,
|
|
},
|
|
pending,
|
|
consumed,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("valid migration with unrelated breakage returned a lifecycle error: %v", err)
|
|
}
|
|
if report.Compatible {
|
|
t.Fatalf("unrelated breakage was hidden by migration approval: %#v", report)
|
|
}
|
|
for _, comparison := range report.Comparisons {
|
|
if !hasFlagChange(comparison.Blocking, test.kind, "dws chat send", test.flag) {
|
|
t.Fatalf("reference %q blocking=%#v, want %s for --%s", comparison.Reference, comparison.Blocking, test.kind, test.flag)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsRetainsConsumedReceiptUntilStableCatchesUp(t *testing.T) {
|
|
before := testFlagMigrationSnapshot(false, true)
|
|
after := testFlagMigrationSnapshot(true, true)
|
|
consumed := testFlagMigrationManifest(FlagMigrationConsumed)
|
|
|
|
report, err := CompareAllWithFlagMigrations(
|
|
after,
|
|
map[string]Snapshot{
|
|
"merge-base": after,
|
|
"stable": before,
|
|
},
|
|
consumed,
|
|
consumed,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("consumed receipt was rejected while stable still needed it: %v", err)
|
|
}
|
|
if !report.Compatible {
|
|
t.Fatalf("stable comparison did not honor the consumed receipt: %#v", report.Comparisons)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsRequiresCleanupAfterAllReferencesCatchUp(t *testing.T) {
|
|
after := testFlagMigrationSnapshot(true, true)
|
|
consumed := testFlagMigrationManifest(FlagMigrationConsumed)
|
|
references := map[string]Snapshot{
|
|
"merge-base": after,
|
|
"stable": after,
|
|
}
|
|
|
|
if report, err := CompareAllWithFlagMigrations(after, references, consumed, consumed); err == nil {
|
|
t.Fatalf("stale consumed receipt was accepted after every reference caught up: %#v", report)
|
|
}
|
|
|
|
report, err := CompareAllWithFlagMigrations(after, references, consumed, testEmptyFlagMigrationManifest())
|
|
if err != nil {
|
|
t.Fatalf("cleanup of stale consumed receipt was rejected: %v", err)
|
|
}
|
|
if !report.Compatible {
|
|
t.Fatalf("cleanup-only candidate was incompatible: %#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 testFlagMigrationSnapshot(after, includeUnrelated bool) Snapshot {
|
|
legacy := Flag{
|
|
Name: "legacy-id",
|
|
Shorthand: "l",
|
|
Type: "string",
|
|
NoOpt: "auto",
|
|
Required: true,
|
|
}
|
|
flags := []Flag{legacy}
|
|
if after {
|
|
legacy.Required = false
|
|
legacy.Hidden = true
|
|
legacy.AliasOf = "message-id"
|
|
flags = []Flag{
|
|
legacy,
|
|
{
|
|
Name: "message-id",
|
|
Type: "string",
|
|
Required: true,
|
|
},
|
|
}
|
|
}
|
|
if includeUnrelated {
|
|
flags = append(flags, Flag{
|
|
Name: "format",
|
|
Shorthand: "f",
|
|
Type: "string",
|
|
NoOpt: "json",
|
|
})
|
|
}
|
|
return testSnapshot(
|
|
testCommand("dws"),
|
|
testCommand("dws chat send", flags...),
|
|
)
|
|
}
|
|
|
|
func testFlagMigrationManifest(state string) FlagMigrationManifest {
|
|
return FlagMigrationManifest{
|
|
Version: FlagMigrationManifestVersion,
|
|
Migrations: []FlagMigration{
|
|
{
|
|
Command: "dws chat send",
|
|
Legacy: FlagMigrationSide{
|
|
Name: "legacy-id",
|
|
Before: FlagMigrationState{
|
|
Present: true,
|
|
Type: "string",
|
|
Required: true,
|
|
Shorthand: "l",
|
|
NoOpt: "auto",
|
|
Scope: "local",
|
|
},
|
|
After: FlagMigrationState{
|
|
Present: true,
|
|
Type: "string",
|
|
Hidden: true,
|
|
Shorthand: "l",
|
|
NoOpt: "auto",
|
|
Scope: "local",
|
|
AliasOf: "message-id",
|
|
},
|
|
},
|
|
Canonical: FlagMigrationSide{
|
|
Name: "message-id",
|
|
Before: FlagMigrationState{},
|
|
After: FlagMigrationState{
|
|
Present: true,
|
|
Type: "string",
|
|
Required: true,
|
|
Scope: "local",
|
|
},
|
|
},
|
|
State: state,
|
|
Reason: "rename the public flag while preserving the executable legacy alias",
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func testEmptyFlagMigrationManifest() FlagMigrationManifest {
|
|
return FlagMigrationManifest{
|
|
Version: FlagMigrationManifestVersion,
|
|
Migrations: []FlagMigration{},
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|