Compare commits

...
29 changed files with 6605 additions and 181 deletions
+28 -17
View File
@@ -1370,36 +1370,47 @@ jobs:
if [ -z "$base_ref" ] || [ "$base_ref" = "0000000000000000000000000000000000000000" ]; then
base_ref="$(git rev-parse HEAD^)"
fi
candidate_ref="$(git rev-parse 'HEAD^{commit}')"
if [ "$GITHUB_EVENT_NAME" = "pull_request" ] && [ "$candidate_ref" != "$PR_HEAD_SHA" ]; then
echo "Compatibility checkout $candidate_ref does not match PR head $PR_HEAD_SHA" >&2
exit 1
fi
git rev-parse --verify "${base_ref}^{commit}" >/dev/null
stable_ref="$(git tag --merged "$base_ref" --list 'v[0-9]*' --sort=-version:refname | awk 'index($0, "-") == 0 { print; exit }')"
. ./scripts/release/release-lib.sh
stable_ref=""
for tag in $(git tag --merged "$base_ref" --list 'v*' --sort=-version:refname); do
release_is_stable_version "$tag" || continue
if git rev-parse --verify --quiet "refs/tags/withdrawn/$tag" >/dev/null; then
continue
fi
stable_ref="$tag"
break
done
if [ -z "$stable_ref" ]; then
echo "No stable release tag is reachable from compatibility base $base_ref" >&2
exit 1
fi
git rev-parse --verify "${stable_ref}^{commit}" >/dev/null
echo "COMPATIBILITY_BASE_REF=$base_ref" >> "$GITHUB_ENV"
echo "COMPATIBILITY_STABLE_REF=$stable_ref" >> "$GITHUB_ENV"
printf '%s\n' \
"COMPATIBILITY_BASE_REF=$base_ref" \
"COMPATIBILITY_STABLE_REF=$stable_ref" \
"COMPATIBILITY_CANDIDATE_REF=$candidate_ref" >> "$GITHUB_ENV"
- name: Check historical commands and help compatibility
- name: Check historical commands, help, and complete CLI compatibility
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
run: |
make authoritative-interface-integrity \
BASE_REF="$COMPATIBILITY_BASE_REF"
if [ "$(git rev-parse "${COMPATIBILITY_BASE_REF}^{commit}")" != "$(git rev-parse "${COMPATIBILITY_STABLE_REF}^{commit}")" ]; then
make authoritative-interface-integrity \
BASE_REF="$COMPATIBILITY_STABLE_REF"
fi
- name: Check complete CLI command compatibility
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
run: |
./scripts/policy/check-command-compatibility.sh \
--base-ref "$COMPATIBILITY_BASE_REF" \
--stable-ref "$COMPATIBILITY_STABLE_REF"
BASE_REF="$COMPATIBILITY_BASE_REF" \
STABLE_REF="$COMPATIBILITY_STABLE_REF" \
CANDIDATE_REF="$COMPATIBILITY_CANDIDATE_REF"
- name: Check complete Schema compatibility
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
run: make schema-compatibility BASE_REF="$COMPATIBILITY_BASE_REF"
run: |
make schema-compatibility \
BASE_REF="$COMPATIBILITY_BASE_REF" \
STABLE_REF="$COMPATIBILITY_STABLE_REF" \
CANDIDATE_REF="$COMPATIBILITY_CANDIDATE_REF"
- name: Check skill command references
if: ${{ needs.lint.outputs.changelog_only != 'true' && needs.lint.outputs.docs_only != 'true' && (needs.lint.outputs.full_suite == 'true' || needs.lint.outputs.interface_sensitive == 'true') }}
+11 -3
View File
@@ -68,9 +68,17 @@ coverage is additionally selected for platform-sensitive code.
3. Include both the commands/results and user-visible or contract-level
behavior evidence in the PR description.
4. Run `./scripts/policy/check-command-surface.sh --strict` when command
paths/flags change. CI also runs
`./scripts/policy/check-command-compatibility.sh --base-ref <main-ref> --stable-ref <latest-GA-tag>`
against both the target branch and latest stable release.
paths/flags change. CI resolves the exact merge-base, latest reachable
non-withdrawn stable GA tag, and committed candidate SHA, then enters the single compatibility
decision seam through
`make authoritative-interface-integrity BASE_REF=<merge-base> STABLE_REF=<latest-GA-tag> CANDIDATE_REF=<candidate-sha>`.
The Make target delegates to the authoritative wrapper; CI does not invoke a
second comparator or the legacy fixture checker. See
[CLI flag compatibility migration governance](docs/cli-interface-flag-migrations.md)
for the reviewed two-stage `pending` → `consumed` lifecycle.
Agent-visible flag migrations must also run
`make schema-compatibility BASE_REF=<merge-base> STABLE_REF=<latest-GA-tag> CANDIDATE_REF=<candidate-sha>`;
it consumes the same base-owned ledger rather than a second exception list.
5. Run `./scripts/policy/check-generated-drift.sh` when generated artifacts may
change.
6. Run `./scripts/release/verify-package-managers.sh` when packaging or
+25 -8
View File
@@ -24,13 +24,13 @@ help:
@printf " make format-check - Check all repository Go source files with gofmt\n"
@printf " make fmt - Format all repository Go source files\n"
@printf " make policy - Check the built dws plus open-source and Schema policies\n"
@printf " make interface-integrity - Check historical commands and help contracts still work\n"
@printf " make authoritative-interface-integrity BASE_REF=<ref> - Check the Git-owned PR merge-base\n"
@printf " make interface-integrity [BASE_REF=<ref>] [STABLE_REF=<tag>] [CANDIDATE_REF=<ref>] - Check authoritative CLI history\n"
@printf " make authoritative-interface-integrity BASE_REF=<ref> [STABLE_REF=<tag>] [CANDIDATE_REF=<ref>] - Check Git-owned CLI history\n"
@printf " make coverage-gate BASE_REF=<ref> - Enforce overall non-regression and 100%% changed-code coverage\n"
@printf " make coverage-gate-platform BASE_REF=<ref> PROFILE=<file> - Enforce 100%% native changed-code coverage\n"
@printf " make update-interface-baseline - Add new CLI contracts without removing history\n"
@printf " make reset-interface-baseline - DANGEROUS: replace all CLI compatibility history\n"
@printf " make schema-compatibility BASE_REF=<ref> - Check the complete Schema contract against the PR merge-base\n"
@printf " make update-interface-baseline - Update the non-authoritative CLI smoke fixture\n"
@printf " make reset-interface-baseline - DANGEROUS: replace the non-authoritative CLI smoke fixture\n"
@printf " make schema-compatibility BASE_REF=<ref> [STABLE_REF=<tag>] [CANDIDATE_REF=<ref>] - Check the authoritative Schema history\n"
@printf " make skill-command-integrity - Check dws commands referenced by skills exist\n"
@printf " make skill-context-budget - Check generated Skill drift and common-path context budgets\n"
@printf " make multi-im-skill-chain-integrity - Check reviewed IM intents keep one default Skill route\n"
@@ -103,10 +103,22 @@ edition-test:
$(GO) test -v -count=1 ./pkg/editiontest/...
interface-integrity:
@./scripts/policy/check-interface-baseline.sh
@base_ref="$(BASE_REF)"; \
candidate_ref="$(CANDIDATE_REF)"; \
if [ -z "$$base_ref" ]; then base_ref="origin/main"; fi; \
if [ -z "$$candidate_ref" ]; then candidate_ref="HEAD"; fi; \
./scripts/policy/check-authoritative-interface-baselines.sh \
--base-ref "$$base_ref" \
--stable-ref "$(STABLE_REF)" \
--candidate-ref "$$candidate_ref"
authoritative-interface-integrity:
@./scripts/policy/check-authoritative-interface-baselines.sh --base-ref "$(BASE_REF)"
@candidate_ref="$(CANDIDATE_REF)"; \
if [ -z "$$candidate_ref" ]; then candidate_ref="HEAD"; fi; \
./scripts/policy/check-authoritative-interface-baselines.sh \
--base-ref "$(BASE_REF)" \
--stable-ref "$(STABLE_REF)" \
--candidate-ref "$$candidate_ref"
coverage-gate:
@./scripts/policy/check-coverage-gate.sh --base-ref "$(BASE_REF)" --scope-buildable
@@ -121,7 +133,12 @@ reset-interface-baseline:
@./scripts/policy/check-interface-baseline.sh --reset
schema-compatibility:
@./scripts/policy/check-authoritative-schema-compatibility.sh --base-ref "$(BASE_REF)"
@candidate_ref="$(CANDIDATE_REF)"; \
if [ -z "$$candidate_ref" ]; then candidate_ref="HEAD"; fi; \
./scripts/policy/check-authoritative-schema-compatibility.sh \
--base-ref "$(BASE_REF)" \
--stable-ref "$(STABLE_REF)" \
--candidate-ref "$$candidate_ref"
skill-command-integrity:
@./scripts/policy/check-skill-commands.sh
+91 -2
View File
@@ -17,18 +17,23 @@
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/i18n"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot"
"github.com/spf13/cobra"
)
var newRootCommand = func() *cobra.Command { return app.NewRootCommand() }
func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}
@@ -112,7 +117,11 @@ func runGenerate(args []string, stdout, stderr io.Writer) error {
defer i18n.SetLang(previousLang)
i18n.SetLang("en")
snapshot := interfacesnapshot.Capture(app.NewRootCommand())
root := newRootCommand()
snapshot := interfacesnapshot.Capture(root)
if err := validateHelpRendering(root, snapshot); err != nil {
return err
}
if *output == "-" {
return interfacesnapshot.Write(stdout, snapshot)
}
@@ -138,6 +147,16 @@ func runCompare(args []string, stdout, stderr io.Writer) (bool, error) {
currentPath := flags.String("current", "", "candidate snapshot path")
basePath := flags.String("base", "", "target main/development baseline snapshot path")
stablePath := flags.String("stable", "", "latest stable GA snapshot path")
approvedMigrationsPath := flags.String(
"approved-flag-migrations",
"",
"merge-base-owned approved flag migration manifest",
)
candidateMigrationsPath := flags.String(
"candidate-flag-migrations",
"",
"candidate flag migration manifest",
)
if err := flags.Parse(args); err != nil {
return false, err
}
@@ -150,6 +169,14 @@ func runCompare(args []string, stdout, stderr io.Writer) (bool, error) {
if *basePath == "" && *stablePath == "" {
return false, fmt.Errorf("compare requires --base, --stable, or both")
}
if (*approvedMigrationsPath == "") != (*candidateMigrationsPath == "") {
return false, fmt.Errorf(
"--approved-flag-migrations and --candidate-flag-migrations must be provided together",
)
}
if *approvedMigrationsPath != "" && (*basePath == "" || *stablePath == "") {
return false, fmt.Errorf("flag migration compare requires both --base and --stable")
}
current, err := readSnapshot(*currentPath)
if err != nil {
@@ -170,6 +197,25 @@ func runCompare(args []string, stdout, stderr io.Writer) (bool, error) {
}
report := interfacesnapshot.CompareAll(current, references)
if *approvedMigrationsPath != "" {
approved, readErr := readFlagMigrationManifest(*approvedMigrationsPath)
if readErr != nil {
return false, fmt.Errorf("read approved flag migrations: %w", readErr)
}
candidate, readErr := readFlagMigrationManifest(*candidateMigrationsPath)
if readErr != nil {
return false, fmt.Errorf("read candidate flag migrations: %w", readErr)
}
report, err = interfacesnapshot.CompareAllWithFlagMigrations(
current,
references,
approved,
candidate,
)
if err != nil {
return false, fmt.Errorf("validate flag migration lifecycle: %w", err)
}
}
encoder := json.NewEncoder(stdout)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
@@ -179,6 +225,49 @@ func runCompare(args []string, stdout, stderr io.Writer) (bool, error) {
return report.Compatible, nil
}
func readFlagMigrationManifest(path string) (interfacesnapshot.FlagMigrationManifest, error) {
file, err := os.Open(filepath.Clean(path))
if err != nil {
return interfacesnapshot.FlagMigrationManifest{}, err
}
defer file.Close()
return interfacesnapshot.ReadFlagMigrationManifest(file)
}
func validateHelpRendering(root *cobra.Command, snapshot interfacesnapshot.Snapshot) error {
for _, command := range snapshot.Commands {
path := strings.TrimPrefix(command.Path, "dws")
resolved, remaining, err := root.Find(strings.Fields(path))
if err != nil || len(remaining) != 0 || resolved == nil {
return fmt.Errorf("resolve %q before help rendering: remaining=%v error=%v", command.Path, remaining, err)
}
if err := renderCommandHelp(resolved); err != nil {
return fmt.Errorf("render %q help: %w", command.Path, err)
}
}
return nil
}
func renderCommandHelp(command *cobra.Command) (err error) {
var stdout, stderr bytes.Buffer
command.InitDefaultHelpFlag()
command.SetOut(&stdout)
command.SetErr(&stderr)
defer func() {
if recovered := recover(); recovered != nil {
err = fmt.Errorf("help renderer panicked: %v", recovered)
}
}()
command.HelpFunc()(command, []string{})
if stderr.Len() > 0 {
return fmt.Errorf("help renderer wrote an error: %s", strings.TrimSpace(stderr.String()))
}
if stdout.Len() == 0 {
return fmt.Errorf("help renderer produced empty output")
}
return nil
}
func readSnapshot(path string) (interfacesnapshot.Snapshot, error) {
file, err := os.Open(filepath.Clean(path))
if err != nil {
@@ -191,5 +280,5 @@ func readSnapshot(path string) (interfacesnapshot.Snapshot, error) {
func printUsage(w io.Writer) {
fmt.Fprintln(w, "usage:")
fmt.Fprintln(w, " interface-snapshot generate [--output FILE]")
fmt.Fprintln(w, " interface-snapshot compare --current FILE [--base FILE] [--stable FILE]")
fmt.Fprintln(w, " interface-snapshot compare --current FILE [--base FILE] [--stable FILE] [--approved-flag-migrations FILE --candidate-flag-migrations FILE]")
}
+447
View File
@@ -15,11 +15,16 @@ package main
import (
"bytes"
"errors"
"io"
"os"
"path/filepath"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/testseam"
"github.com/spf13/cobra"
)
func TestCrossPlatformCoverageRunGenerateCapturesActualRootOffline(t *testing.T) {
@@ -56,6 +61,24 @@ func TestCrossPlatformCoverageRunGenerateCapturesActualRootOffline(t *testing.T)
}
}
func TestCrossPlatformCoverageRunGenerateRejectsHelpRenderingFailure(t *testing.T) {
testseam.Swap(t, &newRootCommand, func() *cobra.Command {
root := &cobra.Command{Use: "dws"}
root.SetHelpFunc(func(command *cobra.Command, _ []string) {
_, _ = io.WriteString(command.ErrOrStderr(), "injected help failure")
})
return root
})
var stdout, stderr bytes.Buffer
if exitCode := run([]string{"generate"}, &stdout, &stderr); exitCode != 2 {
t.Fatalf("run(generate) exit=%d stderr=%s", exitCode, stderr.String())
}
if !strings.Contains(stderr.String(), "injected help failure") {
t.Fatalf("run(generate) stderr=%q", stderr.String())
}
}
func TestCrossPlatformCoverageRunCompareUsesBothSnapshotInputsAndExitCode(t *testing.T) {
current := commandSnapshot("dws")
mergeBase := commandSnapshot("dws")
@@ -83,6 +106,368 @@ func TestCrossPlatformCoverageRunCompareUsesBothSnapshotInputsAndExitCode(t *tes
}
}
func TestCrossPlatformCoverageRunCompareEnforcesBaseOwnedFlagMigrationLifecycle(t *testing.T) {
dir := t.TempDir()
before := flagMigrationSnapshot(false)
after := flagMigrationSnapshot(true)
currentPath := writeSnapshot(t, dir, "current.json", after)
basePath := writeSnapshot(t, dir, "base.json", before)
stablePath := writeSnapshot(t, dir, "stable.json", before)
approvedPath := writeManifest(t, dir, "approved.json", flagMigrationManifestJSON("pending"))
candidatePath := writeManifest(t, dir, "candidate.json", flagMigrationManifestJSON("consumed"))
var stdout, stderr bytes.Buffer
exitCode := run([]string{
"compare",
"--current", currentPath,
"--base", basePath,
"--stable", stablePath,
"--approved-flag-migrations", approvedPath,
"--candidate-flag-migrations", candidatePath,
}, &stdout, &stderr)
if exitCode != 0 {
t.Fatalf("exact base-owned migration exit=%d stderr=%s", exitCode, stderr.String())
}
if !bytes.Contains(stdout.Bytes(), []byte(`"compatible": true`)) {
t.Fatalf("exact migration report is not compatible:\n%s", stdout.String())
}
stdout.Reset()
stderr.Reset()
emptyApproved := writeManifest(t, dir, "empty-approved.json", `{"version":1,"migrations":[]}`)
exitCode = run([]string{
"compare",
"--current", currentPath,
"--base", basePath,
"--stable", stablePath,
"--approved-flag-migrations", emptyApproved,
"--candidate-flag-migrations", candidatePath,
}, &stdout, &stderr)
if exitCode != 2 || !strings.Contains(stderr.String(), "must start pending") {
t.Fatalf("candidate self-approval exit=%d stdout=%s stderr=%s", exitCode, stdout.String(), stderr.String())
}
}
func TestCrossPlatformCoverageRunCompareRequiresBothFlagMigrationInputs(t *testing.T) {
dir := t.TempDir()
currentPath := writeSnapshot(t, dir, "current.json", commandSnapshot("dws"))
basePath := writeSnapshot(t, dir, "base.json", commandSnapshot("dws"))
approvedPath := writeManifest(t, dir, "approved.json", `{"version":1,"migrations":[]}`)
var stdout, stderr bytes.Buffer
exitCode := run([]string{
"compare",
"--current", currentPath,
"--base", basePath,
"--approved-flag-migrations", approvedPath,
}, &stdout, &stderr)
if exitCode != 2 || !strings.Contains(stderr.String(), "must be provided together") {
t.Fatalf("one-sided migration input exit=%d stdout=%s stderr=%s", exitCode, stdout.String(), stderr.String())
}
}
func TestCrossPlatformCoverageRunCompareRequiresBothReferencesForFlagMigrations(t *testing.T) {
dir := t.TempDir()
currentPath := writeSnapshot(t, dir, "current.json", commandSnapshot("dws"))
basePath := writeSnapshot(t, dir, "base.json", commandSnapshot("dws"))
stablePath := writeSnapshot(t, dir, "stable.json", commandSnapshot("dws"))
approvedPath := writeManifest(t, dir, "approved.json", `{"version":1,"migrations":[]}`)
candidatePath := writeManifest(t, dir, "candidate.json", `{"version":1,"migrations":[]}`)
tests := []struct {
name string
args []string
}{
{
name: "missing stable",
args: []string{"--base", basePath},
},
{
name: "missing base",
args: []string{"--stable", stablePath},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
args := []string{"compare", "--current", currentPath}
args = append(args, test.args...)
args = append(args,
"--approved-flag-migrations", approvedPath,
"--candidate-flag-migrations", candidatePath,
)
var stdout, stderr bytes.Buffer
exitCode := run(args, &stdout, &stderr)
if exitCode != 2 || !strings.Contains(stderr.String(), "requires both --base and --stable") {
t.Fatalf("one-reference migration compare exit=%d stdout=%s stderr=%s", exitCode, stdout.String(), stderr.String())
}
})
}
}
func TestCrossPlatformCoverageRunPrintsUsageForMissingAndUnknownCommands(t *testing.T) {
tests := []struct {
name string
args []string
wantStderr []string
}{
{
name: "missing command",
args: nil,
wantStderr: []string{"usage:", "interface-snapshot generate", "--approved-flag-migrations"},
},
{
name: "unknown command",
args: []string{"unknown"},
wantStderr: []string{`unknown command "unknown"`, "usage:", "interface-snapshot compare"},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
if exitCode := run(test.args, &stdout, &stderr); exitCode != 2 {
t.Fatalf("run(%v) exit=%d, want 2", test.args, exitCode)
}
if stdout.Len() != 0 {
t.Fatalf("run(%v) unexpectedly wrote stdout: %s", test.args, stdout.String())
}
for _, want := range test.wantStderr {
if !strings.Contains(stderr.String(), want) {
t.Errorf("run(%v) stderr missing %q:\n%s", test.args, want, stderr.String())
}
}
})
}
}
func TestCrossPlatformCoverageRunRejectsInvalidSubcommandArguments(t *testing.T) {
tests := []struct {
name string
args []string
want string
}{
{name: "generate unknown flag", args: []string{"generate", "--unknown"}, want: "flag provided but not defined"},
{name: "generate positional", args: []string{"generate", "unexpected"}, want: "generate accepts no positional arguments"},
{name: "compare unknown flag", args: []string{"compare", "--unknown"}, want: "flag provided but not defined"},
{name: "compare positional", args: []string{"compare", "unexpected"}, want: "compare accepts no positional arguments"},
{name: "compare missing current", args: []string{"compare", "--base", "base.json"}, want: "compare requires --current"},
{name: "compare missing reference", args: []string{"compare", "--current", "current.json"}, want: "compare requires --base, --stable, or both"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
if exitCode := run(test.args, &stdout, &stderr); exitCode != 2 {
t.Fatalf("run(%v) exit=%d, want 2", test.args, exitCode)
}
if !strings.Contains(stderr.String(), test.want) {
t.Fatalf("run(%v) stderr missing %q:\n%s", test.args, test.want, stderr.String())
}
})
}
}
func TestCrossPlatformCoverageRunGenerateRejectsUnsafeOutputPath(t *testing.T) {
outputDirectory := t.TempDir()
var stdout, stderr bytes.Buffer
exitCode := run([]string{"generate", "--output", outputDirectory}, &stdout, &stderr)
if exitCode != 2 || !strings.Contains(stderr.String(), "create snapshot") {
t.Fatalf("directory output exit=%d stdout=%s stderr=%s", exitCode, stdout.String(), stderr.String())
}
}
func TestCrossPlatformCoverageRunGenerateReportsTemporaryDirectoryFailure(t *testing.T) {
missingTempRoot := filepath.Join(t.TempDir(), "missing")
for _, name := range []string{"TMPDIR", "TMP", "TEMP"} {
t.Setenv(name, missingTempRoot)
}
var stdout, stderr bytes.Buffer
exitCode := run([]string{"generate"}, &stdout, &stderr)
if exitCode != 2 || !strings.Contains(stderr.String(), "create isolated home") {
t.Fatalf("invalid temporary root exit=%d stdout=%s stderr=%s", exitCode, stdout.String(), stderr.String())
}
}
func TestCrossPlatformCoverageRunCompareReportsSnapshotReadFailures(t *testing.T) {
dir := t.TempDir()
validPath := writeSnapshot(t, dir, "valid.json", commandSnapshot("dws"))
missingPath := filepath.Join(dir, "missing.json")
tests := []struct {
name string
args []string
want string
}{
{
name: "current",
args: []string{"compare", "--current", missingPath, "--base", validPath},
want: "read current snapshot",
},
{
name: "main",
args: []string{"compare", "--current", validPath, "--base", missingPath},
want: "read main/development baseline snapshot",
},
{
name: "stable",
args: []string{"compare", "--current", validPath, "--stable", missingPath},
want: "read stable snapshot",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
if exitCode := run(test.args, &stdout, &stderr); exitCode != 2 {
t.Fatalf("run(compare) exit=%d, want 2", exitCode)
}
if !strings.Contains(stderr.String(), test.want) {
t.Fatalf("stderr missing %q:\n%s", test.want, stderr.String())
}
})
}
}
func TestCrossPlatformCoverageRunCompareReportsEachManifestReadFailure(t *testing.T) {
dir := t.TempDir()
snapshotPath := writeSnapshot(t, dir, "snapshot.json", commandSnapshot("dws"))
validManifest := writeManifest(t, dir, "valid-manifest.json", `{"version":1,"migrations":[]}`)
invalidManifest := writeManifest(t, dir, "invalid-manifest.json", `{`)
tests := []struct {
name string
approved string
candidate string
want string
}{
{
name: "approved manifest",
approved: invalidManifest,
candidate: validManifest,
want: "read approved flag migrations",
},
{
name: "candidate manifest",
approved: validManifest,
candidate: invalidManifest,
want: "read candidate flag migrations",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
exitCode := run([]string{
"compare",
"--current", snapshotPath,
"--base", snapshotPath,
"--stable", snapshotPath,
"--approved-flag-migrations", test.approved,
"--candidate-flag-migrations", test.candidate,
}, &stdout, &stderr)
if exitCode != 2 || !strings.Contains(stderr.String(), test.want) {
t.Fatalf("%s exit=%d stdout=%s stderr=%s", test.name, exitCode, stdout.String(), stderr.String())
}
})
}
}
func TestCrossPlatformCoverageRunCompareReportsOutputFailure(t *testing.T) {
dir := t.TempDir()
snapshotPath := writeSnapshot(t, dir, "snapshot.json", commandSnapshot("dws"))
var stderr bytes.Buffer
exitCode := run([]string{
"compare",
"--current", snapshotPath,
"--base", snapshotPath,
}, failingWriter{}, &stderr)
if exitCode != 2 || !strings.Contains(stderr.String(), "write comparison report") {
t.Fatalf("comparison output failure exit=%d stderr=%s", exitCode, stderr.String())
}
}
func TestCrossPlatformCoverageReadHelpersRejectMissingAndInvalidInputs(t *testing.T) {
dir := t.TempDir()
missingPath := filepath.Join(dir, "missing.json")
invalidPath := filepath.Join(dir, "invalid.json")
if err := os.WriteFile(invalidPath, []byte(`{`), 0o600); err != nil {
t.Fatalf("write invalid fixture: %v", err)
}
if _, err := readSnapshot(missingPath); err == nil {
t.Fatal("readSnapshot(missing) unexpectedly succeeded")
}
if _, err := readSnapshot(invalidPath); err == nil {
t.Fatal("readSnapshot(invalid) unexpectedly succeeded")
}
if _, err := readFlagMigrationManifest(missingPath); err == nil {
t.Fatal("readFlagMigrationManifest(missing) unexpectedly succeeded")
}
if _, err := readFlagMigrationManifest(invalidPath); err == nil {
t.Fatal("readFlagMigrationManifest(invalid) unexpectedly succeeded")
}
}
func TestCrossPlatformCoverageValidateHelpRenderingReportsResolveError(t *testing.T) {
root := &cobra.Command{Use: "dws"}
err := validateHelpRendering(root, commandSnapshot("dws missing"))
if err == nil || !strings.Contains(err.Error(), `resolve "dws missing" before help rendering`) {
t.Fatalf("validateHelpRendering resolve error = %v", err)
}
}
func TestCrossPlatformCoverageValidateHelpRenderingReportsTemplateError(t *testing.T) {
root := &cobra.Command{Use: "dws"}
root.SetHelpTemplate(`{{index .Commands 99}}`)
err := validateHelpRendering(root, commandSnapshot("dws"))
if err == nil || !strings.Contains(err.Error(), `render "dws" help`) {
t.Fatalf("validateHelpRendering template error = %v", err)
}
}
func TestCrossPlatformCoverageValidateHelpRenderingRecoversTemplatePanic(t *testing.T) {
root := &cobra.Command{Use: "dws"}
root.SetHelpTemplate("{{")
err := validateHelpRendering(root, commandSnapshot("dws"))
if err == nil || !strings.Contains(err.Error(), `render "dws" help`) {
t.Fatalf("validateHelpRendering template panic = %v", err)
}
}
func TestCrossPlatformCoverageValidateHelpRenderingRejectsCustomHelpStderr(t *testing.T) {
root := &cobra.Command{Use: "dws"}
root.SetHelpFunc(func(command *cobra.Command, _ []string) {
_, _ = io.WriteString(command.ErrOrStderr(), "injected help failure")
})
err := validateHelpRendering(root, commandSnapshot("dws"))
if err == nil || !strings.Contains(err.Error(), "injected help failure") {
t.Fatalf("validateHelpRendering custom stderr = %v", err)
}
}
func TestCrossPlatformCoverageValidateHelpRenderingRejectsEmptyOutput(t *testing.T) {
root := &cobra.Command{Use: "dws"}
root.SetHelpFunc(func(*cobra.Command, []string) {})
err := validateHelpRendering(root, commandSnapshot("dws"))
if err == nil || !strings.Contains(err.Error(), "empty") {
t.Fatalf("validateHelpRendering empty output = %v", err)
}
}
func TestCrossPlatformCoverageValidateHelpRenderingAcceptsNormalOutput(t *testing.T) {
root := &cobra.Command{Use: "dws", Short: "root command"}
if err := validateHelpRendering(root, commandSnapshot("dws")); err != nil {
t.Fatalf("validateHelpRendering normal output: %v", err)
}
}
type failingWriter struct{}
func (failingWriter) Write([]byte) (int, error) {
return 0, errors.New("injected write failure")
}
var _ io.Writer = failingWriter{}
func commandSnapshot(paths ...string) interfacesnapshot.Snapshot {
commands := make([]interfacesnapshot.Command, 0, len(paths))
for _, path := range paths {
@@ -120,6 +505,68 @@ func writeSnapshot(t *testing.T, dir, name string, snapshot interfacesnapshot.Sn
return path
}
func writeManifest(t *testing.T, dir, name, contents string) string {
t.Helper()
path := filepath.Join(dir, name)
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
t.Fatalf("write %s: %v", path, err)
}
return path
}
func flagMigrationSnapshot(after bool) interfacesnapshot.Snapshot {
legacy := interfacesnapshot.Flag{
Name: "legacy-id",
Shorthand: "l",
Type: "string",
Default: "",
NoOpt: "auto",
Required: true,
}
flags := []interfacesnapshot.Flag{legacy}
if after {
legacy.Required = false
legacy.Hidden = true
legacy.AliasOf = "message-id"
flags = []interfacesnapshot.Flag{
legacy,
{Name: "message-id", Type: "string", Default: "", Required: true},
}
}
return interfacesnapshot.Snapshot{
SchemaVersion: interfacesnapshot.SchemaVersion,
Rules: interfacesnapshot.Rules{
ExcludedCommandSubtrees: []string{},
ExcludedFlags: []string{},
},
Commands: []interfacesnapshot.Command{
{Path: "dws", Runnable: true, Aliases: []string{}, LocalFlags: []interfacesnapshot.Flag{}, InheritedFlags: []interfacesnapshot.Flag{}},
{Path: "dws chat send", Runnable: true, Aliases: []string{}, LocalFlags: flags, InheritedFlags: []interfacesnapshot.Flag{}},
},
}
}
func flagMigrationManifestJSON(state string) string {
return strings.Replace(`{
"version": 1,
"migrations": [{
"command": "dws chat send",
"legacy": {
"name": "legacy-id",
"before": {"present": true, "type": "string", "required": true, "shorthand": "l", "no_opt": "auto", "scope": "local"},
"after": {"present": true, "type": "string", "hidden": true, "shorthand": "l", "no_opt": "auto", "scope": "local", "alias_of": "message-id"}
},
"canonical": {
"name": "message-id",
"before": {"present": false},
"after": {"present": true, "type": "string", "required": true, "scope": "local"}
},
"state": "STATE",
"reason": "reviewed exact migration"
}]
}`, "STATE", state, 1)
}
func hasFlag(flags []interfacesnapshot.Flag, name, flagType string) bool {
for _, flag := range flags {
if flag.Name == name && flag.Type == flagType {
+39 -8
View File
@@ -157,15 +157,33 @@ expected to repeat every CI job locally:
```sh
make build
make policy
make interface-integrity
make authoritative-interface-integrity BASE_REF=<merge-base>
make schema-compatibility BASE_REF=<merge-base>
make interface-integrity BASE_REF=<merge-base> STABLE_REF=<stable-tag> CANDIDATE_REF=<candidate-sha>
make schema-compatibility BASE_REF=<merge-base> STABLE_REF=<stable-tag> CANDIDATE_REF=<candidate-sha>
make skill-command-integrity
make cli-smoke
make mock-mcp-smoke
go test -v -count=1 ./pkg/editiontest/...
```
CI 先解析并核对精确的 merge-base、最近可达且未撤回的 stable GA tag 和已提交的 candidate
SHA,再调用 `make authoritative-interface-integrity`。本地 `make interface-integrity`
与该 CI target 都只委托给同一个 modern authoritative wrapper,不存在第二个比较
入口。省略 `BASE_REF` 时本地 target 默认比较 `origin/main`,省略 `STABLE_REF` 时自动
选择该 base 可达且未撤回的最近 stable GA tag,省略 `CANDIDATE_REF` 时比较已提交的 `HEAD`。
需要逐字复现某次 CI 时,应显式传入该次运行记录的 merge-base、stable tag 和
candidate SHA。
`make update-interface-baseline` / `make reset-interface-baseline` 只维护
`test/fixtures/cli-interface-baseline.txt` 这一份非权威 CLI Smoke fixture。底层旧
`check-interface-baseline.sh` 不再作为本地或 CI 的兼容性审批入口,也不能用于批准
flag 迁移。
Schema compatibility 使用同一组 base、stable、candidate refs 和同一份 base-owned flag
migration ledger。merge-base-owned checker 分别规范化 merge-base 与 stable 的完整
Schema,并让 candidate 对两份历史 contract 独立执行检查;它只把已通过 Interface
lifecycle 的 exact rename 规范化到当前历史副本,不会维护第二份 allowlist,也不会
放宽其他 Schema 历史字段。
For an exact CHANGELOG-only branch:
```sh
@@ -186,11 +204,24 @@ are evaluated by the same block-deduplicating checker; supporting policy and
shortcut profiles contribute to changed-code coverage only. The checked-in
badge is presentation only and is never read as a gate input.
Compatibility checks derive authoritative Interface snapshots from the PR
merge-base and the latest reachable stable release. The candidate cannot bless
a breaking change by editing a fixture. Schema additions are allowed;
historical products, tools, parameters, mappings, positional execution fields,
constraints, and safety semantics remain protected.
CLI 兼容检查只使用 modern Interface Snapshot 这一处权威比较 seam,并从 PR
merge-base 和最近的可达 stable release 生成权威快照。本治理机制合入后,
merge-base 拥有生成器、比较器和已审批迁移清单,因此 candidate 不能通过修改
helper、fixture 或在同一 PR 新增 self-approval 记录来放行 breaking change。首次
bootstrap 仍由 merge-base 已有的 modern helper 做无豁免比较,并只接受 candidate
提交中的规范空清单;完整边界见下方治理文档。
精确的两阶段 flag 迁移生命周期见
[CLI flag 兼容迁移治理](cli-interface-flag-migrations.md)。治理 PR 只能在
surface 未变化时新增 `pending`;后续产品 PR 达到审批的精确 surface 后,才能
消费 base-owned 记录并改为 `consumed`。在 main 与 stable 都达到 after 状态前
必须保留该回执,之后再由单独 PR 清理。机制只放行记录中的 legacy
visible-to-hidden,以及 canonical required 新增或提升;删除、type、scope、
shorthand、no-opt 和任何无关漂移仍然阻塞。Schema 可以新增;历史 product、
tool、parameter、mapping、positional execution、constraint 与 safety 语义继续
受保护。`alias_of` 只是一项由 `FlagSpec.Aliases` 产生的框架关系证据,不是 payload
等价证明;产品 PR 仍须证明 canonical 与 legacy 的最终运行 payload 等价并在 transport
前拒绝冲突输入。当前迁移清单为空,不授权 PR #904。
## Required GitHub repository settings
+176
View File
@@ -0,0 +1,176 @@
# CLI flag 兼容迁移治理
本文定义一种受控迁移:保留旧 flag 的可执行兼容性,但把它从 Help 与 Agent Schema 中隐藏,并将新的规范 flag 提升为必填。它只解决这一种精确变更,不是通用 breaking-change 豁免。
同名 flag 的精确类型迁移属于另一类评审机制,只能进入
`internal/interfacesnapshot/reviewed.go` 与 legacy smoke helper 的镜像表;flag rename
只能进入本文的 JSON lifecycle ledger。一项迁移不得跨两种机制组合授权。
## 唯一比较入口与信任边界
PR 与本地兼容性审批的唯一权威比较入口是 modern Interface Snapshot:
- `cmd/interface-snapshot` 生成和比较快照;
- `internal/interfacesnapshot` 实现兼容规则和迁移生命周期;
- `scripts/policy/check-command-compatibility.sh` 组装 candidate、PR merge-base 和最近可达且未撤回的 stable GA 三份快照;
- `scripts/policy/check-authoritative-interface-baselines.sh` 只保留为 Makefile 的兼容包装,不再维护第二套判断逻辑。
紧随其后的 Schema compatibility 不是第二份审批清单。它从同一 merge-base-owned
ledger 和同一组三方 Interface Snapshot 取得已经完成 lifecycle 校验的
authorization。merge-base-owned checker 会分别规范化 merge-base 与 stable 的完整
Schema,并让 candidate 对两份历史 contract 独立执行检查;授权的 flag rename 只会
精确投影到当前被检查的历史副本。candidate 不能为 CLI 与 Schema 分别提供两套例外。
`make interface-integrity` 也调用上述 authoritative wrapper;默认 base 为
`origin/main`,stable 可由包装脚本自动解析,candidate 默认为已提交的 `HEAD`。旧
`scripts/policy/check-interface-baseline.sh` 只供
`make update-interface-baseline` / `make reset-interface-baseline` 维护非权威 CLI
Smoke fixture,不参与迁移审批。
直接调用 `interface-snapshot compare` 时,只要提供 migration manifest 参数,就必须
同时提供 `--base` 与 `--stable`;核心 lifecycle 也拒绝缺失 stable 的非空清单,避免
调用方因漏传历史参考而提前清理 consumed receipt。
PR merge-base 同时拥有快照生成器、比较器和已审批清单。门禁用这套 base-owned helper 检查同一个已提交 candidate revision、merge-base 与 stable,candidate 不能通过修改自己的 Go 比较 helper 来放宽规则。candidate 中的清单只参与迁移状态流转,不能批准同一个 PR 引入的接口变化。首次引入本机制时,merge-base 尚无迁移解析器;bootstrap 会用 merge-base 已有的 modern Interface Snapshot 做不带豁免的普通比较,并只接受 candidate 中逐字匹配的空清单,不会让 candidate 新增的 comparator 决定本 PR 是否兼容。bootstrap 无法让旧 helper 证明新治理实现本身正确,因此本治理 PR 的新 parser、lifecycle、launcher 与 hostile tests 仍是必须由真人评审的受保护策略变更;它们合入后才成为后续 PR 的 base-owned authority。
这条边界保护比较规则和审批数据,不是任意代码沙箱。GitHub workflow / launcher 的变更仍由仓库保护规则和真人评审负责;candidate Cobra 构建也会执行 candidate 代码,因此对同一 runner 上的主动恶意代码,需要独立进程或文件系统隔离,不能把本门禁描述成已经解决。
已审批清单固定为:
```text
scripts/policy/interface-migrations/approved-flag-migrations-v1.json
```
清单使用严格 JSON 解析:版本、字段名大小写、JSON 值类型、命令路径和 flag 名都必须精确;拒绝重复键、未知键、scalar `null` 与尾随 JSON 值,`reason` 不能为空;禁止 `*`、`?`、前缀规则或其他 wildcard。当前清单为空,因此本治理 PR **不授权 PR #904 或任何产品接口变化**。
## 两阶段迁移与回执清理
每条迁移以 `(command, legacy flag, canonical flag)` 为唯一精确键,并经历以下生命周期:
| 阶段 | PR 可以做什么 | 必须满足的快照状态 |
|---|---|---|
| 1. 治理审批 | 新增 `state: pending` 的精确记录;不得在同一个 PR 修改产品 surface | candidate 和 merge-base 都与记录中的 `before` 完全一致;该记录不改变 stable 的判断 |
| 2. 产品迁移 | merge-base 已拥有 `pending` 后,按记录一次性切到精确 `after`,并把记录改为 `state: consumed` | legacy 仍存在但由 visible 变 hidden,且声明 `alias_of`;canonical 达到记录的必填状态 |
| 3. 保留回执 | 产品 PR 合入后,如果 stable 仍是 `before`,继续保留 `consumed` | merge-base 或 stable 仍有任一份尚未达到 `after` |
| 4. 单独清理 | 当 merge-base 和 stable 都已经是 `after`,在后续 PR 删除该记录 | 两份参考快照均精确匹配 `after`;继续保留过期回执会被门禁拒绝 |
因此,新增 `pending` 和修改产品 surface 不能发生在同一个 PR;candidate 自己新增的记录不能 self-approve。迁移也不能部分执行:legacy、canonical、`alias_of` 或状态只要有一项不匹配,门禁即失败。
下面只是清单结构示例,不代表已审批命令;实际字段必须从 Interface Snapshot 核对:
```json
{
"version": 1,
"migrations": [
{
"command": "dws chat message recall",
"legacy": {
"name": "msg-id",
"before": {
"present": true,
"type": "string",
"required": true,
"scope": "local"
},
"after": {
"present": true,
"type": "string",
"hidden": true,
"scope": "local",
"alias_of": "message-id"
}
},
"canonical": {
"name": "message-id",
"before": { "present": false },
"after": {
"present": true,
"type": "string",
"required": true,
"scope": "local"
}
},
"state": "pending",
"reason": "保留旧 argv 兼容性,并将规范 flag 设为唯一可见入口"
}
]
}
```
产品迁移 PR 必须保持同一条记录的命令、flag、before/after 和 reason 不变,只把 `pending` 改成 `consumed`。
## `alias_of` 是框架来源的受评审关系证据
`alias_of` 不是 Schema 同义词、参数概念词典或任意文字声明。它只能由 `FlagSpec.Aliases` 写入,并与内部 origin `corecmd.flag_spec_aliases.v1` 成对出现;每次 Interface Integrity 都会在已提交的 detached candidate 上执行源码门禁,禁止其他生产文件写入或复刻这些 evidence token。Interface Snapshot 会验证:
- legacy 与 canonical 位于同一个可执行命令;
- canonical flag 确实存在;
- legacy 与 canonical 类型一致;
- legacy 不是指向自身,也不存在 alias chain;
- legacy 的 after 状态精确指向该记录中的 canonical flag。
通过命令框架声明 `FlagSpec.Aliases` 时,框架会自动注册隐藏的兼容 flag,并写入两项 relation annotation;仅手写 `alias_of`、伪造 origin、重复值或不精确值都会让快照生成失败。不要用 Schema overlay、迁移清单或手写 Cobra annotation 伪造关系。
这项关系证据只证明 legacy/canonical 经过受控框架路径建立关系,不证明最终 transport payload 等价,也不会替产品代码实现命令特有的值同步。当前框架还禁止把
`MarkRequired` 与 `FlagSpec.Aliases` 直接组合,因为 Cobra 的 hard-required
校验只识别 canonical spelling。若产品迁移同时需要 canonical 的 Cobra required
标记和 legacy spelling,产品 PR 必须提供明确的运行时方案,并通过 canonical / legacy
最终 payload 等价、同值输入一致、冲突输入在 transport 前失败、legacy 仍可调用但 Help 隐藏等测试;迁移清单和 relation evidence 都不能替代这些证明。
## 豁免边界
一条 base-owned、状态正确且前后快照精确匹配的记录,只会从普通兼容报告中移除以下两类预期 finding:
1. legacy flag 的 `flag_became_hidden`(visible → hidden);
2. canonical flag 的 `required_flag_added`(新增时即必填)或 `flag_became_required`(已有 flag 从可选变必填)。
以下变化仍按普通兼容规则阻塞,不能被迁移记录掩盖:
- 删除 legacy、canonical、命令或其他 flag;
- flag 类型或迁移记录中的 scope、shorthand、`no_opt` 漂移;
- `alias_of` 缺失、指向变化或 alias chain;
- 命令路径及任何无关的阻塞性接口变化;
- 不精确、部分完成、超出记录范围的 surface 变化。
## Schema 投影边界
Agent-visible command 会把 visible Cobra flag 投影为 Schema parameter,因此合法的
legacy hidden 迁移会同时表现为历史 parameter 消失,constraint member 也可能从
legacy 名改为 canonical 名。Schema adapter 只接受已经由三方 Interface Snapshot
判定为 authorized 的迁移,并按 tool 的精确 `primary_cli_path` 绑定:
- reference 仍处于 `before` 且该 flag 有 Schema surface 时,baseline legacy parameter
必须存在,candidate legacy parameter 必须消失,candidate canonical parameter 必须存在;
如果 baseline 只有 canonical、没有 legacy,则 adapter 不得借 CLI ledger 提升
`required` / `cli_required` 或重写 constraint;
- rename 前后的 `type`、`property`、`interface_type`、default、format、enum 与
`required_when` 必须完全一致;
- `required` / `cli_required` 只能保持不变或按审批从 `false` 提升为 `true`,禁止降低;
- constraint 只允许在同一 tool 内按已枚举的 legacy → canonical map 做 member 替换、
排序与去重;group kind、非迁移 member 或 group 增删仍然阻塞;
- 多个 legacy 指向同一 canonical 时,所有历史 parameter signature 必须一致,否则
fail closed。
adapter 先构造经过上述验证的历史 contract 副本,再调用原 Schema checker;它不会按
错误字符串删除 finding。这样既能处理纯 rename,也能阻止“旧 required 参数改名后意外
变为 optional”或 property 漂移等伪兼容。`consumed` 回执在 merge-base Schema 已经处于
canonical-only `after` 状态时不需要再次投影;adapter 保持 baseline 不变,由原 checker
验证 candidate 是否仍与该 canonical contract 兼容。
## 本地验证
先确保 merge-base 和 stable tag 已在本地,然后运行与 CI 相同的权威门禁:
```sh
make interface-integrity \
BASE_REF=<merge-base> \
STABLE_REF=<stable-tag> \
CANDIDATE_REF=<candidate-sha>
make schema-compatibility \
BASE_REF=<merge-base> \
STABLE_REF=<stable-tag> \
CANDIDATE_REF=<candidate-sha>
```
`STABLE_REF` 必须解析到从该 merge-base 可达的最高未撤回 stable GA tag;primary checker 会按 release contract 独立核对,不能用任意 after commit 或已撤回版本提前清理回执。包装脚本可以在省略时自动解析。`CANDIDATE_REF` 省略时固定为命令启动时的已提交 `HEAD`;评审和复现 CI 时应显式传入 candidate SHA,避免 surface 与清单来自不同 revision。
+12
View File
@@ -558,6 +558,18 @@ func RegisterFlags(cmd *cobra.Command, flags []FlagSpec) {
for _, alias := range flag.Aliases {
RegisterFlag(cmd, flag.Kind, alias, "", flag.Usage+" (alias)")
_ = cmd.Flags().MarkHidden(alias)
if registered := cmd.Flags().Lookup(alias); registered != nil {
runtimeannotate.SetFlagAnnotation(
registered,
runtimeannotate.AnnotationFlagAliasOf,
flag.Name,
)
runtimeannotate.SetFlagAnnotation(
registered,
runtimeannotate.AnnotationFlagAliasOrigin,
runtimeannotate.FlagAliasOriginCorecmdV1,
)
}
}
if flag.MarkRequired {
_ = cmd.MarkFlagRequired(flag.Name)
+7 -1
View File
@@ -90,11 +90,17 @@ func TestCrossPlatformCoverageRegisterFlagsAllKinds(t *testing.T) {
}
}
// Aliases are registered with the main kind and hidden.
for _, alias := range []string{"i-alias", "sl-alias"} {
for alias, canonical := range map[string]string{"i-alias": "i", "sl-alias": "sl"} {
f := cmd.Flags().Lookup(alias)
if f == nil || !f.Hidden {
t.Fatalf("alias %q = %#v, want registered+hidden", alias, f)
}
if got := f.Annotations[runtimeannotate.AnnotationFlagAliasOf]; len(got) != 1 || got[0] != canonical {
t.Fatalf("alias %q annotation = %#v, want alias_of %q", alias, got, canonical)
}
if got := f.Annotations[runtimeannotate.AnnotationFlagAliasOrigin]; len(got) != 1 || got[0] != runtimeannotate.FlagAliasOriginCorecmdV1 {
t.Fatalf("alias %q origin = %#v, want corecmd FlagSpec marker", alias, got)
}
}
if cmd.Flags().Lookup("i-alias").Value.Type() != "int" {
t.Fatal("int alias must be registered as int")
@@ -0,0 +1,25 @@
// 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 runtimeannotate
// CLI flag alias evidence is kept in this narrow file so the base-owned
// Interface Snapshot helper can add the protocol constants to an older stable
// worktree without replacing that revision's complete runtimeannotate package.
// Only corecmd.FlagSpec.Aliases writes the exact origin; neither field is a
// Schema synonym or final payload-equivalence proof.
const (
AnnotationFlagAliasOf = "dws.compat.alias_of"
AnnotationFlagAliasOrigin = "dws.compat.alias_origin"
FlagAliasOriginCorecmdV1 = "corecmd.flag_spec_aliases.v1"
)
+9
View File
@@ -229,6 +229,15 @@ func compareEffectiveFlags(result *Comparison, acceptedPath string, oldCommand,
After: newFlag.NoOpt,
})
}
if oldFlag.AliasOf != "" && newFlag.AliasOf != oldFlag.AliasOf {
result.Blocking = append(result.Blocking, Change{
Kind: "flag_alias_target_changed",
Path: acceptedPath,
Flag: name,
Before: oldFlag.AliasOf,
After: newFlag.AliasOf,
})
}
if !oldFlag.Hidden && newFlag.Hidden {
result.Blocking = append(result.Blocking, Change{
Kind: "flag_became_hidden",
+408 -2
View File
@@ -251,6 +251,40 @@ func TestCrossPlatformCoverageCompareBlocksCallableMetadataRegressions(t *testin
}
}
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)),
@@ -314,6 +348,58 @@ func TestCrossPlatformCoverageCompareAllRequiresBothReferencesToPass(t *testing.
}
}
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.
@@ -426,6 +512,12 @@ func TestCrossPlatformCoverageCompareReviewedFlagTypeChangeRejectsBundledRegress
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 {
@@ -501,8 +593,8 @@ var reviewedEntryPattern = regexp.MustCompile(
const interfaceBaselineReviewedPath = "../../scripts/policy/interface-baseline/reviewed.go"
// The two copies of the table must stay identical, otherwise a migration passes
// one gate and is blocked by the other in the same CI job.
// 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 {
@@ -533,6 +625,234 @@ func TestCrossPlatformCoverageReviewedFlagTypeTableMatchesInterfaceBaseline(t *t
}
}
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(&current)
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,
@@ -571,6 +891,92 @@ 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 {
+696
View File
@@ -0,0 +1,696 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package interfacesnapshot
import (
"bytes"
"encoding/json"
"fmt"
"io"
"reflect"
"strings"
)
const FlagMigrationManifestVersion = 1
const (
FlagMigrationPending = "pending"
FlagMigrationConsumed = "consumed"
)
type FlagMigrationManifest struct {
Version int `json:"version"`
Migrations []FlagMigration `json:"migrations"`
}
type FlagMigration struct {
Command string `json:"command"`
Legacy FlagMigrationSide `json:"legacy"`
Canonical FlagMigrationSide `json:"canonical"`
State string `json:"state"`
Reason string `json:"reason"`
}
type FlagMigrationSide struct {
Name string `json:"name"`
Before FlagMigrationState `json:"before"`
After FlagMigrationState `json:"after"`
}
type FlagMigrationState struct {
Present bool `json:"present"`
Type string `json:"type,omitempty"`
Required bool `json:"required,omitempty"`
Hidden bool `json:"hidden,omitempty"`
Shorthand string `json:"shorthand,omitempty"`
NoOpt string `json:"no_opt,omitempty"`
Scope string `json:"scope,omitempty"`
AliasOf string `json:"alias_of,omitempty"`
}
func ReadFlagMigrationManifest(r io.Reader) (FlagMigrationManifest, error) {
data, err := io.ReadAll(r)
if err != nil {
return FlagMigrationManifest{}, fmt.Errorf("read flag migration manifest: %w", err)
}
var manifest FlagMigrationManifest
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(&manifest); err != nil {
return FlagMigrationManifest{}, err
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
if err == nil {
return FlagMigrationManifest{}, fmt.Errorf("flag migration manifest contains trailing multiple JSON values")
}
return FlagMigrationManifest{}, fmt.Errorf("read trailing flag migration manifest data: %w", err)
}
if err := validateFlagMigrationJSONSchema(data); err != nil {
return FlagMigrationManifest{}, err
}
if err := manifest.Validate(); err != nil {
return FlagMigrationManifest{}, err
}
return manifest, nil
}
func validateFlagMigrationJSONSchema(data []byte) error {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := validateMigrationJSONValue(decoder, "$", reflect.TypeOf(FlagMigrationManifest{})); err != nil {
return err
}
if token, err := decoder.Token(); err != io.EOF {
if err == nil {
return fmt.Errorf("flag migration manifest contains trailing JSON value %v", token)
}
return fmt.Errorf("read trailing flag migration manifest data: %w", err)
}
return nil
}
func validateMigrationJSONValue(decoder *json.Decoder, path string, schema reflect.Type) error {
token, err := decoder.Token()
if err != nil {
return fmt.Errorf("read flag migration manifest value at %s: %w", path, err)
}
switch schema.Kind() {
case reflect.Struct:
if delimiter, ok := token.(json.Delim); !ok || delimiter != '{' {
return migrationJSONTypeError(path, schema, token)
}
fields := migrationJSONFields(schema)
seen := make(map[string]bool, len(fields))
for decoder.More() {
keyToken, keyErr := decoder.Token()
if keyErr != nil {
return fmt.Errorf("read flag migration manifest field at %s: %w", path, keyErr)
}
// encoding/json guarantees object member names are string tokens.
key := keyToken.(string)
fieldSchema, exists := fields[key]
if !exists {
for canonical := range fields {
if strings.EqualFold(key, canonical) {
return fmt.Errorf(
"flag migration manifest contains non-canonical field %q at %s (want %q)",
key,
path,
canonical,
)
}
}
return fmt.Errorf("flag migration manifest contains unknown field %q at %s", key, path)
}
if seen[key] {
return fmt.Errorf("flag migration manifest contains duplicate field %q at %s", key, path)
}
seen[key] = true
if err := validateMigrationJSONValue(decoder, path+"."+key, fieldSchema); err != nil {
return err
}
}
if _, closeErr := decoder.Token(); closeErr != nil {
return fmt.Errorf("close flag migration manifest object at %s: %w", path, closeErr)
}
return nil
case reflect.Slice:
if delimiter, ok := token.(json.Delim); !ok || delimiter != '[' {
return migrationJSONTypeError(path, schema, token)
}
for index := 0; decoder.More(); index++ {
if err := validateMigrationJSONValue(decoder, fmt.Sprintf("%s[%d]", path, index), schema.Elem()); err != nil {
return err
}
}
if _, closeErr := decoder.Token(); closeErr != nil {
return fmt.Errorf("close flag migration manifest array at %s: %w", path, closeErr)
}
return nil
case reflect.String:
if _, ok := token.(string); !ok {
return migrationJSONTypeError(path, schema, token)
}
return nil
case reflect.Int:
if _, ok := token.(json.Number); !ok {
return migrationJSONTypeError(path, schema, token)
}
return nil
case reflect.Bool:
if _, ok := token.(bool); !ok {
return migrationJSONTypeError(path, schema, token)
}
return nil
default:
return fmt.Errorf("flag migration manifest value at %s has unsupported Go schema type %s", path, schema)
}
}
func migrationJSONFields(schema reflect.Type) map[string]reflect.Type {
fields := make(map[string]reflect.Type, schema.NumField())
for index := 0; index < schema.NumField(); index++ {
field := schema.Field(index)
name := field.Tag.Get("json")
if comma := strings.IndexByte(name, ','); comma >= 0 {
name = name[:comma]
}
if name == "-" {
continue
}
if name == "" {
name = field.Name
}
fields[name] = field.Type
}
return fields
}
func migrationJSONTypeError(path string, want reflect.Type, token json.Token) error {
return fmt.Errorf(
"flag migration manifest value at %s must be %s, got %s",
path,
migrationJSONKindDescription(want),
migrationJSONTokenDescription(token),
)
}
func migrationJSONKindDescription(schema reflect.Type) string {
switch schema.Kind() {
case reflect.Struct:
return "an object"
case reflect.Slice:
return "an array"
case reflect.String:
return "a string"
case reflect.Int:
return "a number"
case reflect.Bool:
return "a boolean"
default:
return "the declared JSON type"
}
}
func migrationJSONTokenDescription(token json.Token) string {
if token == nil {
return "null"
}
if delimiter, ok := token.(json.Delim); ok {
switch delimiter {
case '{':
return "object"
case '[':
return "array"
default:
return fmt.Sprintf("delimiter %q", delimiter)
}
}
switch token.(type) {
case string:
return "string"
case json.Number, float64:
return "number"
case bool:
return "boolean"
default:
return fmt.Sprintf("%T", token)
}
}
func (m FlagMigrationManifest) Validate() error {
if m.Version != FlagMigrationManifestVersion {
return fmt.Errorf(
"unsupported flag migration manifest version %d (want %d)",
m.Version,
FlagMigrationManifestVersion,
)
}
if m.Migrations == nil {
return fmt.Errorf("flag migration manifest migrations must be an array")
}
seen := make(map[string]bool, len(m.Migrations))
legacyTargets := make(map[string]string, len(m.Migrations))
for index, migration := range m.Migrations {
if err := migration.validate(); err != nil {
return fmt.Errorf("flag migration %d: %w", index, err)
}
key := migration.key()
if seen[key] {
return fmt.Errorf("flag migration %d duplicates %s", index, key)
}
seen[key] = true
legacyKey := migration.Command + "\x00" + migration.Legacy.Name
if canonical, exists := legacyTargets[legacyKey]; exists && canonical != migration.Canonical.Name {
return fmt.Errorf(
"flag migration %d maps %s --%s to both --%s and --%s",
index,
migration.Command,
migration.Legacy.Name,
canonical,
migration.Canonical.Name,
)
}
legacyTargets[legacyKey] = migration.Canonical.Name
}
return nil
}
func (m FlagMigration) validate() error {
if !isExactCommandPath(m.Command) {
return fmt.Errorf("command must be an exact command path rooted at dws: %q", m.Command)
}
if !isExactFlagName(m.Legacy.Name) {
return fmt.Errorf("legacy name must be an exact legacy flag: %q", m.Legacy.Name)
}
if !isExactFlagName(m.Canonical.Name) {
return fmt.Errorf("canonical name must be an exact canonical flag: %q", m.Canonical.Name)
}
if m.Legacy.Name == m.Canonical.Name {
return fmt.Errorf("legacy and canonical flags must differ: --%s", m.Legacy.Name)
}
if strings.TrimSpace(m.Reason) == "" {
return fmt.Errorf("migration must include a non-empty reason")
}
if m.Reason != strings.TrimSpace(m.Reason) {
return fmt.Errorf("migration reason must be trimmed")
}
if m.State != FlagMigrationPending && m.State != FlagMigrationConsumed {
return fmt.Errorf("invalid state %q", m.State)
}
if err := m.Legacy.Before.validate("legacy before"); err != nil {
return err
}
if err := m.Legacy.After.validate("legacy after"); err != nil {
return err
}
if err := m.Canonical.Before.validate("canonical before"); err != nil {
return err
}
if err := m.Canonical.After.validate("canonical after"); err != nil {
return err
}
if !m.Legacy.Before.Present || !m.Legacy.After.Present {
return fmt.Errorf("legacy flag must remain present before and after migration")
}
if m.Legacy.Before.Hidden || !m.Legacy.After.Hidden {
return fmt.Errorf("legacy flag must migrate exactly from visible to hidden")
}
if !m.Canonical.After.Present {
return fmt.Errorf("canonical flag must be present after migration")
}
if m.Canonical.After.Hidden {
return fmt.Errorf("canonical flag must remain visible")
}
if !m.Canonical.After.Required {
return fmt.Errorf("canonical flag must be required after migration")
}
if m.Canonical.Before.Present && m.Canonical.Before.Required {
return fmt.Errorf("canonical flag must be absent or optional before migration")
}
if m.Legacy.After.AliasOf != m.Canonical.Name {
return fmt.Errorf(
"legacy flag after state must declare alias_of %q",
m.Canonical.Name,
)
}
if m.Legacy.Before.AliasOf != "" || m.Canonical.Before.AliasOf != "" || m.Canonical.After.AliasOf != "" {
return fmt.Errorf("alias_of is only valid on the legacy after state")
}
if m.Canonical.Before.Present {
if m.Canonical.Before.Type != m.Canonical.After.Type {
return fmt.Errorf("canonical flag type must remain unchanged")
}
if m.Canonical.Before.Shorthand != m.Canonical.After.Shorthand {
return fmt.Errorf("canonical flag shorthand must remain unchanged")
}
if m.Canonical.Before.NoOpt != m.Canonical.After.NoOpt {
return fmt.Errorf("canonical flag no_opt must remain unchanged")
}
if m.Canonical.Before.Scope != m.Canonical.After.Scope {
return fmt.Errorf("canonical flag scope must remain unchanged")
}
}
if m.Legacy.Before.Type != m.Legacy.After.Type || m.Legacy.After.Type != m.Canonical.After.Type {
return fmt.Errorf("legacy and canonical flag types must match exactly")
}
if m.Legacy.Before.Shorthand != m.Legacy.After.Shorthand {
return fmt.Errorf("legacy flag shorthand must remain unchanged")
}
if m.Legacy.Before.NoOpt != m.Legacy.After.NoOpt {
return fmt.Errorf("legacy flag no_opt must remain unchanged")
}
if m.Legacy.Before.Scope != m.Legacy.After.Scope {
return fmt.Errorf("legacy flag scope must remain unchanged")
}
return nil
}
func (s FlagMigrationState) validate(label string) error {
if !s.Present {
if s.Type != "" || s.Required || s.Hidden || s.Shorthand != "" || s.NoOpt != "" || s.Scope != "" || s.AliasOf != "" {
return fmt.Errorf("%s absent state must not declare flag attributes", label)
}
return nil
}
if strings.TrimSpace(s.Type) == "" {
return fmt.Errorf("%s present state requires type", label)
}
if s.Scope != "local" && s.Scope != "inherited" {
return fmt.Errorf("%s present state has invalid scope %q", label, s.Scope)
}
return nil
}
func (m FlagMigration) key() string {
return m.Command + "\x00" + m.Legacy.Name + "\x00" + m.Canonical.Name
}
func isExactCommandPath(path string) bool {
return path != "" &&
path == strings.TrimSpace(path) &&
strings.Join(strings.Fields(path), " ") == path &&
(path == "dws" || strings.HasPrefix(path, "dws ")) &&
!strings.ContainsAny(path, "*?[]{}")
}
func isExactFlagName(name string) bool {
return name != "" &&
name == strings.TrimSpace(name) &&
!strings.HasPrefix(name, "-") &&
!strings.ContainsAny(name, "*?[]{} /\t\r\n")
}
// CompareAllWithFlagMigrations applies the ordinary compatibility policy and
// then consumes only exact, merge-base-owned flag migrations. Candidate-owned
// records participate in the lifecycle check, but never authorize their own
// interface change.
func CompareAllWithFlagMigrations(
current Snapshot,
references map[string]Snapshot,
authority FlagMigrationManifest,
candidate FlagMigrationManifest,
) (Report, error) {
authorizations, err := AuthorizeFlagMigrations(current, references, authority, candidate)
if err != nil {
return Report{}, err
}
report := CompareAll(current, references)
if len(authorizations) == 0 {
return report, nil
}
for index := range report.Comparisons {
comparison := &report.Comparisons[index]
reference := references[comparison.Reference]
filtered := comparison.Blocking[:0]
for _, change := range comparison.Blocking {
if flagMigrationAuthorizesChange(current, reference, change, authorizations) {
continue
}
filtered = append(filtered, change)
}
comparison.Blocking = filtered
comparison.Compatible = len(filtered) == 0
}
report.Compatible = true
for _, comparison := range report.Comparisons {
if !comparison.Compatible {
report.Compatible = false
break
}
}
return report, nil
}
// AuthorizeFlagMigrations validates both snapshots and manifests, enforces the
// base-owned migration lifecycle, and returns only approvals that may authorize
// the current exact interface transition. A non-empty lifecycle requires both a
// main/merge-base authority reference and a stable reference. Candidate-added
// records never appear in the returned authorization set.
func AuthorizeFlagMigrations(
current Snapshot,
references map[string]Snapshot,
authority FlagMigrationManifest,
candidate FlagMigrationManifest,
) ([]FlagMigration, error) {
if err := current.Validate(); err != nil {
return nil, fmt.Errorf("validate current interface snapshot: %w", err)
}
for label, snapshot := range references {
if err := snapshot.Validate(); err != nil {
return nil, fmt.Errorf("validate %s interface snapshot: %w", label, err)
}
}
if err := authority.Validate(); err != nil {
return nil, fmt.Errorf("validate approved flag migrations: %w", err)
}
if err := candidate.Validate(); err != nil {
return nil, fmt.Errorf("validate candidate flag migrations: %w", err)
}
return evaluateFlagMigrationLifecycle(current, references, authority, candidate)
}
type flagMigrationPhase string
const (
flagMigrationBefore flagMigrationPhase = "before"
flagMigrationAfter flagMigrationPhase = "after"
flagMigrationPartial flagMigrationPhase = "partial"
)
func evaluateFlagMigrationLifecycle(
current Snapshot,
references map[string]Snapshot,
authority FlagMigrationManifest,
candidate FlagMigrationManifest,
) ([]FlagMigration, error) {
if len(authority.Migrations) == 0 && len(candidate.Migrations) == 0 {
return nil, nil
}
if _, ok := references["stable"]; !ok {
return nil, fmt.Errorf("flag migration lifecycle requires a stable reference")
}
mergeBase, label, ok := flagMigrationAuthoritySnapshot(references)
if !ok {
return nil, fmt.Errorf("flag migration lifecycle requires a main or merge-base reference")
}
authorityByKey := flagMigrationIndex(authority)
candidateByKey := flagMigrationIndex(candidate)
authorizations := make([]FlagMigration, 0, len(authority.Migrations))
for _, approved := range authority.Migrations {
basePhase := matchFlagMigrationPhase(mergeBase, approved)
wantBasePhase := flagMigrationBefore
if approved.State == FlagMigrationConsumed {
wantBasePhase = flagMigrationAfter
}
if basePhase != wantBasePhase {
return nil, fmt.Errorf(
"approved flag migration %s is %s in %s, want exact %s state for %s",
approved.displayKey(),
basePhase,
label,
wantBasePhase,
approved.State,
)
}
proposed, exists := candidateByKey[approved.key()]
if exists && !sameFlagMigrationApproval(approved, proposed) {
return nil, fmt.Errorf("candidate modified base-owned flag migration %s", approved.displayKey())
}
currentPhase := matchFlagMigrationPhase(current, approved)
switch approved.State {
case FlagMigrationPending:
if !exists {
return nil, fmt.Errorf("candidate removed pending flag migration %s", approved.displayKey())
}
switch currentPhase {
case flagMigrationBefore:
if proposed.State != FlagMigrationPending {
return nil, fmt.Errorf("candidate falsely consumed unchanged flag migration %s", approved.displayKey())
}
case flagMigrationAfter:
if proposed.State != FlagMigrationConsumed {
return nil, fmt.Errorf("candidate completed flag migration %s without marking it consumed", approved.displayKey())
}
authorizations = append(authorizations, approved)
default:
return nil, fmt.Errorf("candidate partially applied flag migration %s", approved.displayKey())
}
case FlagMigrationConsumed:
if currentPhase != flagMigrationAfter {
return nil, fmt.Errorf("candidate drifted from consumed flag migration %s", approved.displayKey())
}
allReferencesAfter := true
for _, reference := range references {
if matchFlagMigrationPhase(reference, approved) != flagMigrationAfter {
allReferencesAfter = false
break
}
}
if allReferencesAfter {
if exists {
return nil, fmt.Errorf("consumed flag migration %s is stale after all references reached the after state", approved.displayKey())
}
continue
}
if !exists {
return nil, fmt.Errorf("candidate removed consumed flag migration %s before every reference reached the after state", approved.displayKey())
}
if proposed.State != FlagMigrationConsumed {
return nil, fmt.Errorf("candidate changed consumed flag migration %s back to pending", approved.displayKey())
}
authorizations = append(authorizations, approved)
}
}
for _, proposed := range candidate.Migrations {
if _, exists := authorityByKey[proposed.key()]; exists {
continue
}
if proposed.State != FlagMigrationPending {
return nil, fmt.Errorf("candidate-added flag migration %s must start pending", proposed.displayKey())
}
if matchFlagMigrationPhase(mergeBase, proposed) != flagMigrationBefore {
return nil, fmt.Errorf("candidate-added flag migration %s does not match the merge-base before state", proposed.displayKey())
}
if matchFlagMigrationPhase(current, proposed) != flagMigrationBefore {
return nil, fmt.Errorf("candidate-added flag migration %s cannot authorize its own interface change", proposed.displayKey())
}
}
return authorizations, nil
}
func flagMigrationAuthoritySnapshot(references map[string]Snapshot) (Snapshot, string, bool) {
for _, label := range []string{"merge-base", "main"} {
if snapshot, ok := references[label]; ok {
return snapshot, label, true
}
}
return Snapshot{}, "", false
}
func flagMigrationIndex(manifest FlagMigrationManifest) map[string]FlagMigration {
index := make(map[string]FlagMigration, len(manifest.Migrations))
for _, migration := range manifest.Migrations {
index[migration.key()] = migration
}
return index
}
func sameFlagMigrationApproval(left, right FlagMigration) bool {
left.State = ""
right.State = ""
return reflect.DeepEqual(left, right)
}
func matchFlagMigrationPhase(snapshot Snapshot, migration FlagMigration) flagMigrationPhase {
command, exists := commandIndex(snapshot)[migration.Command]
if !exists {
return flagMigrationPartial
}
legacy := flagMigrationStateForCommand(command, migration.Legacy.Name)
canonical := flagMigrationStateForCommand(command, migration.Canonical.Name)
if legacy == migration.Legacy.Before && canonical == migration.Canonical.Before {
return flagMigrationBefore
}
if legacy == migration.Legacy.After && canonical == migration.Canonical.After {
return flagMigrationAfter
}
return flagMigrationPartial
}
func flagMigrationStateForCommand(command Command, name string) FlagMigrationState {
for _, flag := range command.LocalFlags {
if flag.Name == name {
return flagMigrationState(flag, "local")
}
}
for _, flag := range command.InheritedFlags {
if flag.Name == name {
return flagMigrationState(flag, "inherited")
}
}
return FlagMigrationState{}
}
func flagMigrationState(flag Flag, scope string) FlagMigrationState {
return FlagMigrationState{
Present: true,
Type: flag.Type,
Required: flag.Required,
Hidden: flag.Hidden,
Shorthand: flag.Shorthand,
NoOpt: flag.NoOpt,
Scope: scope,
AliasOf: flag.AliasOf,
}
}
func flagMigrationAuthorizesChange(
current Snapshot,
reference Snapshot,
change Change,
authorizations []FlagMigration,
) bool {
canonicalPath := acceptedPathIndex(reference)[change.Path]
if canonicalPath == "" {
canonicalPath = change.Path
}
for _, migration := range authorizations {
if canonicalPath != migration.Command ||
matchFlagMigrationPhase(reference, migration) != flagMigrationBefore ||
matchFlagMigrationPhase(current, migration) != flagMigrationAfter {
continue
}
if change.Flag == migration.Legacy.Name && change.Kind == "flag_became_hidden" {
return true
}
if change.Flag != migration.Canonical.Name {
continue
}
if !migration.Canonical.Before.Present &&
migration.Canonical.After.Required &&
change.Kind == "required_flag_added" {
return true
}
if migration.Canonical.Before.Present &&
!migration.Canonical.Before.Required &&
migration.Canonical.After.Required &&
change.Kind == "flag_became_required" {
return true
}
}
return false
}
func (m FlagMigration) displayKey() string {
return fmt.Sprintf("%q --%s -> --%s", m.Command, m.Legacy.Name, m.Canonical.Name)
}
@@ -0,0 +1,966 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package interfacesnapshot
import (
"encoding/json"
"reflect"
"strings"
"testing"
)
func TestCrossPlatformCoverageFlagMigrationStrictJSONSchemaErrors(t *testing.T) {
tests := []struct {
name string
input string
wantErr string
}{
{
name: "schema validator rejects a trailing value",
input: coverageManifestJSON() + ` {}`,
wantErr: "trailing JSON value",
},
{
name: "schema validator reports malformed trailing data",
input: coverageManifestJSON() + ` ?`,
wantErr: "read trailing flag migration manifest data",
},
{
name: "schema validator reports an empty document",
input: "",
wantErr: "read flag migration manifest value at $",
},
{
name: "schema validator rejects an unknown field",
input: `{"version":1,"migrations":[],"unexpected":true}`,
wantErr: `unknown field "unexpected"`,
},
{
name: "schema validator reports a malformed field token",
input: `{"`,
wantErr: "flag migration manifest",
},
{
name: "schema validator reports an unclosed object",
input: `{"version":1`,
wantErr: "close flag migration manifest object",
},
{
name: "schema validator reports an unclosed array",
input: `{"version":1,"migrations":[`,
wantErr: "close flag migration manifest array",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
err := validateFlagMigrationJSONSchema([]byte(test.input))
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("validateFlagMigrationJSONSchema() error = %v, want %q", err, test.wantErr)
}
})
}
}
func TestCrossPlatformCoverageFlagMigrationStrictJSONTypeDiagnostics(t *testing.T) {
tests := []struct {
name string
input string
schema reflect.Type
wantErr string
}{
{
name: "manifest must be an object",
input: `[]`,
schema: reflect.TypeOf(FlagMigrationManifest{}),
wantErr: "must be an object, got array",
},
{
name: "migrations must be an array",
input: `{}`,
schema: reflect.TypeOf([]FlagMigration{}),
wantErr: "must be an array, got object",
},
{
name: "integer rejects string",
input: `"one"`,
schema: reflect.TypeOf(int(0)),
wantErr: "must be a number, got string",
},
{
name: "string rejects number",
input: `1`,
schema: reflect.TypeOf(""),
wantErr: "must be a string, got number",
},
{
name: "string rejects boolean",
input: `false`,
schema: reflect.TypeOf(""),
wantErr: "must be a string, got boolean",
},
{
name: "unsupported schema fails closed",
input: `1`,
schema: reflect.TypeOf(float64(0)),
wantErr: "unsupported Go schema type float64",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
decoder := json.NewDecoder(strings.NewReader(test.input))
decoder.UseNumber()
err := validateMigrationJSONValue(decoder, "$", test.schema)
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("validateMigrationJSONValue() error = %v, want %q", err, test.wantErr)
}
})
}
}
func TestCrossPlatformCoverageFlagMigrationStrictJSONSchemaHelpers(t *testing.T) {
type fieldFixture struct {
DefaultName string
Ignored string `json:"-"`
Tagged string `json:"tagged,omitempty"`
}
fields := migrationJSONFields(reflect.TypeOf(fieldFixture{}))
if _, exists := fields["Ignored"]; exists {
t.Fatal("migrationJSONFields() retained a json:- field")
}
if fields["DefaultName"] != reflect.TypeOf("") || fields["tagged"] != reflect.TypeOf("") {
t.Fatalf("migrationJSONFields() = %#v, want default and tagged string fields", fields)
}
if got := migrationJSONKindDescription(reflect.TypeOf(float64(0))); got != "the declared JSON type" {
t.Fatalf("migrationJSONKindDescription(float64) = %q", got)
}
for _, test := range []struct {
name string
token json.Token
want string
}{
{name: "closing delimiter", token: json.Delim('}'), want: `delimiter "}"`},
{name: "unsupported token", token: struct{}{}, want: "struct {}"},
} {
t.Run(test.name, func(t *testing.T) {
if got := migrationJSONTokenDescription(test.token); got != test.want {
t.Fatalf("migrationJSONTokenDescription() = %q, want %q", got, test.want)
}
})
}
}
func TestCrossPlatformCoverageFlagMigrationManifestParserEdges(t *testing.T) {
tests := []struct {
name string
input string
wantErr string
}{
{
name: "initial malformed JSON",
input: `{`,
wantErr: "unexpected EOF",
},
{
name: "multiple JSON values",
input: coverageManifestJSON() + ` {}`,
wantErr: "trailing multiple JSON values",
},
{
name: "malformed trailing JSON",
input: coverageManifestJSON() + ` {`,
wantErr: "read trailing flag migration manifest data",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := ReadFlagMigrationManifest(strings.NewReader(test.input))
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("ReadFlagMigrationManifest() error = %v, want %q", err, test.wantErr)
}
})
}
}
func TestCrossPlatformCoverageFlagMigrationManifestRejectsEveryContractDrift(t *testing.T) {
optionalCanonical := func() FlagMigrationManifest {
manifest := coverageManifest(FlagMigrationPending)
manifest.Migrations[0].Canonical.Before = FlagMigrationState{
Present: true,
Type: "string",
Scope: "local",
}
return manifest
}
tests := []struct {
name string
make func() FlagMigrationManifest
mutate func(*FlagMigrationManifest)
wantErr string
}{
{
name: "canonical flag name is exact",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.Name = "--message-id" },
wantErr: "canonical name must be an exact canonical flag",
},
{
name: "legacy and canonical names differ",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.Name = m.Migrations[0].Legacy.Name },
wantErr: "legacy and canonical flags must differ",
},
{
name: "legacy before state validates",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.Before.Scope = "global" },
wantErr: "legacy before present state has invalid scope",
},
{
name: "legacy after state validates",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.After.Type = "" },
wantErr: "legacy after present state requires type",
},
{
name: "canonical before state validates",
make: optionalCanonical,
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.Before.Type = "" },
wantErr: "canonical before present state requires type",
},
{
name: "canonical after state validates",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.After.Scope = "" },
wantErr: "canonical after present state has invalid scope",
},
{
name: "legacy before remains present",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.Before = FlagMigrationState{} },
wantErr: "legacy flag must remain present before and after migration",
},
{
name: "legacy after remains present",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.After = FlagMigrationState{} },
wantErr: "legacy flag must remain present before and after migration",
},
{
name: "legacy starts visible",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.Before.Hidden = true },
wantErr: "legacy flag must migrate exactly from visible to hidden",
},
{
name: "legacy ends hidden",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.After.Hidden = false },
wantErr: "legacy flag must migrate exactly from visible to hidden",
},
{
name: "canonical after remains present",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.After = FlagMigrationState{} },
wantErr: "canonical flag must be present after migration",
},
{
name: "legacy after declares alias target",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.After.AliasOf = "other-id" },
wantErr: `legacy flag after state must declare alias_of "message-id"`,
},
{
name: "legacy before cannot declare alias",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.Before.AliasOf = "message-id" },
wantErr: "alias_of is only valid on the legacy after state",
},
{
name: "canonical before cannot declare alias",
make: optionalCanonical,
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.Before.AliasOf = "message-id" },
wantErr: "alias_of is only valid on the legacy after state",
},
{
name: "canonical after cannot declare alias",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.After.AliasOf = "message-id" },
wantErr: "alias_of is only valid on the legacy after state",
},
{
name: "canonical type remains stable",
make: optionalCanonical,
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.Before.Type = "stringSlice" },
wantErr: "canonical flag type must remain unchanged",
},
{
name: "canonical shorthand remains stable",
make: optionalCanonical,
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.Before.Shorthand = "m" },
wantErr: "canonical flag shorthand must remain unchanged",
},
{
name: "canonical no-opt remains stable",
make: optionalCanonical,
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.Before.NoOpt = "auto" },
wantErr: "canonical flag no_opt must remain unchanged",
},
{
name: "canonical scope remains stable",
make: optionalCanonical,
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.Before.Scope = "inherited" },
wantErr: "canonical flag scope must remain unchanged",
},
{
name: "legacy before type matches legacy after",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.Before.Type = "stringSlice" },
wantErr: "legacy and canonical flag types must match exactly",
},
{
name: "legacy after type matches canonical after",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Canonical.After.Type = "stringSlice" },
wantErr: "legacy and canonical flag types must match exactly",
},
{
name: "legacy shorthand remains stable",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.After.Shorthand = "x" },
wantErr: "legacy flag shorthand must remain unchanged",
},
{
name: "legacy no-opt remains stable",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.After.NoOpt = "" },
wantErr: "legacy flag no_opt must remain unchanged",
},
{
name: "legacy scope remains stable",
make: func() FlagMigrationManifest { return coverageManifest(FlagMigrationPending) },
mutate: func(m *FlagMigrationManifest) { m.Migrations[0].Legacy.After.Scope = "inherited" },
wantErr: "legacy flag scope must remain unchanged",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
manifest := test.make()
test.mutate(&manifest)
if err := manifest.Validate(); err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("Validate() error = %v, want %q", err, test.wantErr)
}
})
}
}
func TestCrossPlatformCoverageFlagMigrationManifestRejectsAbsentAttributesAndTargetConflicts(t *testing.T) {
stateMutations := []struct {
name string
mutate func(*FlagMigrationState)
}{
{name: "type", mutate: func(s *FlagMigrationState) { s.Type = "string" }},
{name: "required", mutate: func(s *FlagMigrationState) { s.Required = true }},
{name: "hidden", mutate: func(s *FlagMigrationState) { s.Hidden = true }},
{name: "shorthand", mutate: func(s *FlagMigrationState) { s.Shorthand = "m" }},
{name: "no-opt", mutate: func(s *FlagMigrationState) { s.NoOpt = "auto" }},
{name: "scope", mutate: func(s *FlagMigrationState) { s.Scope = "local" }},
{name: "alias", mutate: func(s *FlagMigrationState) { s.AliasOf = "message-id" }},
}
for _, test := range stateMutations {
t.Run("absent state rejects "+test.name, func(t *testing.T) {
manifest := coverageManifest(FlagMigrationPending)
test.mutate(&manifest.Migrations[0].Canonical.Before)
if err := manifest.Validate(); err == nil || !strings.Contains(err.Error(), "absent state must not declare flag attributes") {
t.Fatalf("Validate() error = %v, want absent-state rejection", err)
}
})
}
manifest := coverageManifest(FlagMigrationPending)
second := coverageManifest(FlagMigrationPending).Migrations[0]
second.Canonical.Name = "canonical-two"
second.Legacy.After.AliasOf = second.Canonical.Name
manifest.Migrations = append(manifest.Migrations, second)
if err := manifest.Validate(); err == nil || !strings.Contains(err.Error(), "to both --message-id and --canonical-two") {
t.Fatalf("Validate() error = %v, want one legacy to two canonical targets rejected", err)
}
}
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsValidatesEveryInput(t *testing.T) {
before := coverageMigrationSnapshot(coverageManifest(FlagMigrationPending).Migrations[0], false, false)
empty := coverageEmptyManifest()
tests := []struct {
name string
current Snapshot
references map[string]Snapshot
authority FlagMigrationManifest
candidate FlagMigrationManifest
wantErr string
}{
{
name: "current snapshot",
current: Snapshot{},
references: map[string]Snapshot{"merge-base": before},
authority: empty,
candidate: empty,
wantErr: "validate current interface snapshot",
},
{
name: "reference snapshot",
current: before,
references: map[string]Snapshot{"merge-base": {}},
authority: empty,
candidate: empty,
wantErr: "validate merge-base interface snapshot",
},
{
name: "authority manifest",
current: before,
references: map[string]Snapshot{"merge-base": before},
authority: FlagMigrationManifest{},
candidate: empty,
wantErr: "validate approved flag migrations",
},
{
name: "candidate manifest",
current: before,
references: map[string]Snapshot{"merge-base": before},
authority: empty,
candidate: FlagMigrationManifest{},
wantErr: "validate candidate flag migrations",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := CompareAllWithFlagMigrations(test.current, test.references, test.authority, test.candidate)
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("CompareAllWithFlagMigrations() error = %v, want %q", err, test.wantErr)
}
})
}
report, err := CompareAllWithFlagMigrations(before, map[string]Snapshot{"merge-base": before}, empty, empty)
if err != nil || !report.Compatible {
t.Fatalf("empty migration lifecycle = (%#v, %v), want compatible", report, err)
}
}
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsLifecycleErrors(t *testing.T) {
pending := coverageManifest(FlagMigrationPending)
consumed := coverageManifest(FlagMigrationConsumed)
empty := coverageEmptyManifest()
migration := pending.Migrations[0]
before := coverageMigrationSnapshot(migration, false, false)
after := coverageMigrationSnapshot(migration, true, false)
partial := coverageMigrationSnapshot(migration, false, false)
partial.Commands[len(partial.Commands)-1].LocalFlags[0].Hidden = true
modified := coverageManifest(FlagMigrationPending)
modified.Migrations[0].Reason = "A different reviewed reason."
tests := []struct {
name string
current Snapshot
references map[string]Snapshot
authority FlagMigrationManifest
candidate FlagMigrationManifest
wantErr string
}{
{
name: "requires merge-base or main",
current: before,
references: map[string]Snapshot{"stable": before},
authority: pending,
candidate: pending,
wantErr: "requires a main or merge-base reference",
},
{
name: "pending authority requires before base",
current: after,
references: map[string]Snapshot{"merge-base": after, "stable": after},
authority: pending,
candidate: pending,
wantErr: "want exact before state for pending",
},
{
name: "consumed authority requires after base",
current: before,
references: map[string]Snapshot{"merge-base": before, "stable": before},
authority: consumed,
candidate: consumed,
wantErr: "want exact after state for consumed",
},
{
name: "missing base command is partial",
current: before,
references: map[string]Snapshot{"merge-base": coverageRootOnlySnapshot(), "stable": before},
authority: pending,
candidate: pending,
wantErr: "is partial in merge-base",
},
{
name: "candidate cannot modify base receipt",
current: before,
references: map[string]Snapshot{"merge-base": before, "stable": before},
authority: pending,
candidate: modified,
wantErr: "candidate modified base-owned flag migration",
},
{
name: "candidate cannot remove pending receipt",
current: before,
references: map[string]Snapshot{"merge-base": before, "stable": before},
authority: pending,
candidate: empty,
wantErr: "candidate removed pending flag migration",
},
{
name: "candidate cannot falsely consume pending receipt",
current: before,
references: map[string]Snapshot{"merge-base": before, "stable": before},
authority: pending,
candidate: consumed,
wantErr: "candidate falsely consumed unchanged flag migration",
},
{
name: "candidate must consume completed receipt",
current: after,
references: map[string]Snapshot{"merge-base": before, "stable": before},
authority: pending,
candidate: pending,
wantErr: "without marking it consumed",
},
{
name: "candidate rejects partial application",
current: partial,
references: map[string]Snapshot{"merge-base": before, "stable": before},
authority: pending,
candidate: consumed,
wantErr: "candidate partially applied flag migration",
},
{
name: "consumed receipt rejects current revert",
current: before,
references: map[string]Snapshot{"merge-base": after, "stable": before},
authority: consumed,
candidate: consumed,
wantErr: "candidate drifted from consumed flag migration",
},
{
name: "consumed receipt cannot be removed early",
current: after,
references: map[string]Snapshot{"merge-base": after, "stable": before},
authority: consumed,
candidate: empty,
wantErr: "before every reference reached the after state",
},
{
name: "consumed receipt cannot revert to pending",
current: after,
references: map[string]Snapshot{"merge-base": after, "stable": before},
authority: consumed,
candidate: pending,
wantErr: "back to pending",
},
{
name: "consumed receipt becomes stale",
current: after,
references: map[string]Snapshot{"merge-base": after, "stable": after},
authority: consumed,
candidate: consumed,
wantErr: "is stale after all references reached the after state",
},
{
name: "candidate-added receipt starts pending",
current: before,
references: map[string]Snapshot{"merge-base": before, "stable": before},
authority: empty,
candidate: consumed,
wantErr: "must start pending",
},
{
name: "candidate-added receipt matches base before",
current: before,
references: map[string]Snapshot{"merge-base": after, "stable": after},
authority: empty,
candidate: pending,
wantErr: "does not match the merge-base before state",
},
{
name: "candidate-added receipt cannot self-authorize",
current: after,
references: map[string]Snapshot{"merge-base": before, "stable": before},
authority: empty,
candidate: pending,
wantErr: "cannot authorize its own interface change",
},
{
name: "missing current command is partial",
current: coverageRootOnlySnapshot(),
references: map[string]Snapshot{"merge-base": before, "stable": before},
authority: pending,
candidate: pending,
wantErr: "candidate partially applied flag migration",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := CompareAllWithFlagMigrations(test.current, test.references, test.authority, test.candidate)
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("CompareAllWithFlagMigrations() error = %v, want %q", err, test.wantErr)
}
})
}
}
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsMainAuthorityAndCleanup(t *testing.T) {
pending := coverageManifest(FlagMigrationPending)
consumed := coverageManifest(FlagMigrationConsumed)
empty := coverageEmptyManifest()
migration := pending.Migrations[0]
before := coverageMigrationSnapshot(migration, false, false)
after := coverageMigrationSnapshot(migration, true, false)
report, err := CompareAllWithFlagMigrations(
after,
map[string]Snapshot{"main": before, "stable": before},
pending,
consumed,
)
if err != nil || !report.Compatible {
t.Fatalf("main-owned migration = (%#v, %v), want compatible", report, err)
}
report, err = CompareAllWithFlagMigrations(
after,
map[string]Snapshot{"merge-base": before, "main": after, "stable": before},
pending,
consumed,
)
if err != nil || !report.Compatible {
t.Fatalf("merge-base precedence = (%#v, %v), want compatible", report, err)
}
report, err = CompareAllWithFlagMigrations(
after,
map[string]Snapshot{"merge-base": after, "stable": after},
consumed,
empty,
)
if err != nil || !report.Compatible {
t.Fatalf("consumed receipt cleanup = (%#v, %v), want compatible", report, err)
}
report, err = CompareAllWithFlagMigrations(
before,
map[string]Snapshot{"merge-base": before, "stable": before},
empty,
pending,
)
if err != nil || !report.Compatible {
t.Fatalf("pending receipt creation = (%#v, %v), want compatible", report, err)
}
}
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsRequiresStableForReceiptCleanup(t *testing.T) {
consumed := coverageManifest(FlagMigrationConsumed)
empty := coverageEmptyManifest()
after := coverageMigrationSnapshot(consumed.Migrations[0], true, false)
report, err := CompareAllWithFlagMigrations(
after,
map[string]Snapshot{"merge-base": after},
consumed,
empty,
)
if err == nil || !strings.Contains(err.Error(), "requires a stable reference") {
t.Fatalf("receipt cleanup without stable = (%#v, %v), want stable-reference error", report, err)
}
}
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsInheritedAndOptionalCanonical(t *testing.T) {
t.Run("inherited exact state", func(t *testing.T) {
pending := coverageManifest(FlagMigrationPending)
pending.Migrations[0].Legacy.Before.Scope = "inherited"
pending.Migrations[0].Legacy.After.Scope = "inherited"
pending.Migrations[0].Canonical.After.Scope = "inherited"
consumed := pending
consumed.Migrations = append([]FlagMigration(nil), pending.Migrations...)
consumed.Migrations[0].State = FlagMigrationConsumed
before := coverageMigrationSnapshot(pending.Migrations[0], false, false)
after := coverageMigrationSnapshot(pending.Migrations[0], true, false)
report, err := CompareAllWithFlagMigrations(
after,
map[string]Snapshot{"merge-base": before, "stable": before},
pending,
consumed,
)
if err != nil || !report.Compatible {
t.Fatalf("inherited migration = (%#v, %v), want compatible", report, err)
}
})
t.Run("existing optional canonical becomes required", func(t *testing.T) {
pending := coverageManifest(FlagMigrationPending)
pending.Migrations[0].Canonical.Before = FlagMigrationState{
Present: true,
Type: "string",
Scope: "local",
}
consumed := pending
consumed.Migrations = append([]FlagMigration(nil), pending.Migrations...)
consumed.Migrations[0].State = FlagMigrationConsumed
before := coverageMigrationSnapshot(pending.Migrations[0], false, false)
after := coverageMigrationSnapshot(pending.Migrations[0], true, false)
ordinary := Compare(after, before, "merge-base")
if !hasFlagChange(ordinary.Blocking, "flag_became_required", pending.Migrations[0].Command, pending.Migrations[0].Canonical.Name) {
t.Fatalf("fixture did not create flag_became_required: %#v", ordinary.Blocking)
}
report, err := CompareAllWithFlagMigrations(
after,
map[string]Snapshot{"merge-base": before, "stable": before},
pending,
consumed,
)
if err != nil || !report.Compatible {
t.Fatalf("optional-to-required migration = (%#v, %v), want compatible", report, err)
}
})
}
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsNormalizesAliasesAndKeepsOtherBreakage(t *testing.T) {
pending := coverageManifest(FlagMigrationPending)
consumed := coverageManifest(FlagMigrationConsumed)
migration := pending.Migrations[0]
before := coverageMigrationSnapshot(migration, false, true)
after := coverageMigrationSnapshot(migration, true, true)
ordinary := Compare(after, before, "merge-base")
if !hasFlagChange(ordinary.Blocking, "flag_became_hidden", "dws chat deliver", migration.Legacy.Name) {
t.Fatalf("alias fixture did not create alias-path finding: %#v", ordinary.Blocking)
}
report, err := CompareAllWithFlagMigrations(
after,
map[string]Snapshot{"merge-base": before, "stable": before},
pending,
consumed,
)
if err != nil || !report.Compatible {
t.Fatalf("alias-path migration = (%#v, %v), want compatible", report, err)
}
beforeWithOther := before
afterWithOther := after
beforeWithOther.Commands = append(beforeWithOther.Commands, coverageCommand("dws files", nil, []Flag{{Name: "format", Type: "string"}}, nil))
afterWithOther.Commands = append(afterWithOther.Commands, coverageCommand("dws files", nil, nil, nil))
report, err = CompareAllWithFlagMigrations(
afterWithOther,
map[string]Snapshot{"merge-base": beforeWithOther, "stable": beforeWithOther},
pending,
consumed,
)
if err != nil {
t.Fatalf("unrelated breakage returned lifecycle error: %v", err)
}
if report.Compatible || !hasFlagChange(report.Comparisons[0].Blocking, "flag_removed", "dws files", "format") {
t.Fatalf("unrelated breakage was hidden: %#v", report)
}
}
func TestCrossPlatformCoverageCompareAllWithFlagMigrationsDoesNotAuthorizePartialReferenceOrRuleDrift(t *testing.T) {
pending := coverageManifest(FlagMigrationPending)
consumed := coverageManifest(FlagMigrationConsumed)
migration := pending.Migrations[0]
before := coverageMigrationSnapshot(migration, false, false)
after := coverageMigrationSnapshot(migration, true, false)
partialReference := coverageMigrationSnapshot(migration, false, false)
partialReference.Commands[len(partialReference.Commands)-1].LocalFlags[0].Hidden = true
report, err := CompareAllWithFlagMigrations(
after,
map[string]Snapshot{"merge-base": before, "stable": partialReference},
pending,
consumed,
)
if err != nil {
t.Fatalf("partial non-authority reference returned lifecycle error: %v", err)
}
if report.Compatible {
t.Fatalf("partial reference was incorrectly authorized: %#v", report)
}
beforeWithoutRoot := coverageMigrationSnapshot(migration, false, false)
afterWithoutRoot := coverageMigrationSnapshot(migration, true, false)
beforeWithoutRoot.Commands = beforeWithoutRoot.Commands[len(beforeWithoutRoot.Commands)-1:]
afterWithoutRoot.Commands = afterWithoutRoot.Commands[len(afterWithoutRoot.Commands)-1:]
afterWithoutRoot.Rules.ExcludedFlags = []string{"help", "version"}
report, err = CompareAllWithFlagMigrations(
afterWithoutRoot,
map[string]Snapshot{"merge-base": beforeWithoutRoot, "stable": beforeWithoutRoot},
pending,
consumed,
)
if err != nil {
t.Fatalf("rule drift returned lifecycle error: %v", err)
}
if report.Compatible || !hasChangeKind(report.Comparisons[0].Blocking, "snapshot_rules_changed") {
t.Fatalf("snapshot rule drift was incorrectly authorized: %#v", report)
}
}
func coverageManifestJSON() string {
return `{
"version": 1,
"migrations": [
{
"command": "dws chat send",
"legacy": {
"name": "legacy-id",
"before": {"present": true, "type": "string", "required": true, "shorthand": "l", "no_opt": "auto", "scope": "local"},
"after": {"present": true, "type": "string", "hidden": true, "shorthand": "l", "no_opt": "auto", "scope": "local", "alias_of": "message-id"}
},
"canonical": {
"name": "message-id",
"before": {"present": false},
"after": {"present": true, "type": "string", "required": true, "scope": "local"}
},
"state": "pending",
"reason": "Reviewed exact flag migration."
}
]
}`
}
func coverageManifest(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: "Reviewed exact flag migration.",
},
},
}
}
func coverageEmptyManifest() FlagMigrationManifest {
return FlagMigrationManifest{Version: FlagMigrationManifestVersion, Migrations: []FlagMigration{}}
}
func coverageMigrationSnapshot(migration FlagMigration, after, commandAlias bool) Snapshot {
legacyState := migration.Legacy.Before
canonicalState := migration.Canonical.Before
if after {
legacyState = migration.Legacy.After
canonicalState = migration.Canonical.After
}
local := []Flag{}
inherited := []Flag{}
add := func(name string, state FlagMigrationState) {
if !state.Present {
return
}
flag := Flag{
Name: name,
Shorthand: state.Shorthand,
Type: state.Type,
NoOpt: state.NoOpt,
Required: state.Required,
Hidden: state.Hidden,
AliasOf: state.AliasOf,
}
if state.Scope == "inherited" {
inherited = append(inherited, flag)
return
}
local = append(local, flag)
}
add(migration.Legacy.Name, legacyState)
add(migration.Canonical.Name, canonicalState)
aliases := []string{}
if commandAlias {
aliases = []string{"deliver"}
}
return coverageSnapshot(
coverageCommand("dws", nil, nil, nil),
coverageCommand("dws chat", nil, nil, nil),
coverageCommand(migration.Command, aliases, local, inherited),
)
}
func coverageRootOnlySnapshot() Snapshot {
return coverageSnapshot(coverageCommand("dws", nil, nil, nil))
}
func coverageSnapshot(commands ...Command) Snapshot {
return Snapshot{
SchemaVersion: SchemaVersion,
Rules: Rules{
ExcludedCommandSubtrees: append([]string(nil), excludedCommandSubtrees...),
ExcludedFlags: []string{"help"},
},
Commands: commands,
}
}
func coverageCommand(path string, aliases []string, local, inherited []Flag) Command {
if aliases == nil {
aliases = []string{}
}
if local == nil {
local = []Flag{}
}
if inherited == nil {
inherited = []Flag{}
}
return Command{
Path: path,
Runnable: true,
Aliases: aliases,
LocalFlags: local,
InheritedFlags: inherited,
}
}
@@ -0,0 +1,252 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package interfacesnapshot
import (
"errors"
"reflect"
"strings"
"testing"
)
type flagMigrationErrorReader struct{}
func (flagMigrationErrorReader) Read([]byte) (int, error) {
return 0, errors.New("injected read failure")
}
const validFlagMigrationManifestJSON = `{
"version": 1,
"migrations": [
{
"command": "dws chat message recall",
"legacy": {
"name": "msg-id",
"before": {"present": true, "type": "string", "required": true, "scope": "local"},
"after": {"present": true, "type": "string", "hidden": true, "scope": "local", "alias_of": "message-id"}
},
"canonical": {
"name": "message-id",
"before": {"present": false},
"after": {"present": true, "type": "string", "required": true, "scope": "local"}
},
"state": "pending",
"reason": "Reviewed message identifier flag migration."
}
]
}`
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsUnknownFields(t *testing.T) {
_, err := ReadFlagMigrationManifest(strings.NewReader(`{
"version": 1,
"migrations": [],
"allow_all_chat_flags": true
}`))
if err == nil || !strings.Contains(err.Error(), "unknown field") {
t.Fatalf("ReadFlagMigrationManifest() error = %v, want unknown field rejection", err)
}
}
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsDuplicateFields(t *testing.T) {
for _, input := range []string{
`{"version": 999, "version": 1, "migrations": []}`,
strings.Replace(validFlagMigrationManifestJSON, `"state": "pending"`, `"state": "consumed", "state": "pending"`, 1),
} {
_, err := ReadFlagMigrationManifest(strings.NewReader(input))
if err == nil || !strings.Contains(err.Error(), "duplicate field") {
t.Fatalf("ReadFlagMigrationManifest() error = %v, want duplicate field rejection", err)
}
}
}
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsNonCanonicalFields(t *testing.T) {
for _, input := range []string{
`{"Version": 1, "migrations": []}`,
`{"version": 1, "Version": 1, "migrations": []}`,
strings.Replace(validFlagMigrationManifestJSON, `"state": "pending"`, `"State": "pending"`, 1),
} {
_, err := ReadFlagMigrationManifest(strings.NewReader(input))
if err == nil || !strings.Contains(err.Error(), "non-canonical field") {
t.Fatalf("ReadFlagMigrationManifest() error = %v, want non-canonical field rejection", err)
}
}
}
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsNullScalars(t *testing.T) {
for _, input := range []string{
strings.Replace(validFlagMigrationManifestJSON, `"required": true`, `"required": null`, 1),
strings.Replace(validFlagMigrationManifestJSON, `"scope": "local"`, `"scope": "local", "shorthand": null`, 1),
`{"version": null, "migrations": []}`,
} {
_, err := ReadFlagMigrationManifest(strings.NewReader(input))
if err == nil || !strings.Contains(err.Error(), "must be") || !strings.Contains(err.Error(), "null") {
t.Fatalf("ReadFlagMigrationManifest() error = %v, want scalar null rejection", err)
}
}
}
func TestCrossPlatformCoverageReadFlagMigrationManifestRejectsWrongJSONTypes(t *testing.T) {
for _, input := range []string{
strings.Replace(validFlagMigrationManifestJSON, `"required": true`, `"required": "true"`, 1),
strings.Replace(validFlagMigrationManifestJSON, `"scope": "local"`, `"scope": false`, 1),
`{"version": "1", "migrations": []}`,
`{"version": 1, "migrations": {}}`,
} {
if _, err := ReadFlagMigrationManifest(strings.NewReader(input)); err == nil {
t.Fatal("ReadFlagMigrationManifest() accepted a field with the wrong JSON type")
}
}
}
func TestCrossPlatformCoverageReadFlagMigrationManifestReportsReaderFailure(t *testing.T) {
_, err := ReadFlagMigrationManifest(flagMigrationErrorReader{})
if err == nil || !strings.Contains(err.Error(), "injected read failure") {
t.Fatalf("ReadFlagMigrationManifest() error = %v, want reader failure", err)
}
}
func TestCrossPlatformCoverageReadFlagMigrationManifestValidatesExactEntries(t *testing.T) {
if _, err := ReadFlagMigrationManifest(strings.NewReader(validFlagMigrationManifestJSON)); err != nil {
t.Fatalf("ReadFlagMigrationManifest(valid) error = %v", err)
}
tests := []struct {
name string
input string
wantErr string
}{
{
name: "unsupported version",
input: strings.Replace(validFlagMigrationManifestJSON, `"version": 1`, `"version": 2`, 1),
wantErr: "unsupported flag migration manifest version",
},
{
name: "missing migrations array",
input: `{"version": 1}`,
wantErr: "migrations must be an array",
},
{
name: "null migrations array",
input: `{"version": 1, "migrations": null}`,
wantErr: "migrations must be an array",
},
{
name: "wildcard command",
input: strings.Replace(validFlagMigrationManifestJSON, "dws chat message recall", "dws chat *", 1),
wantErr: "exact command path",
},
{
name: "wildcard flag",
input: strings.Replace(validFlagMigrationManifestJSON, `"name": "msg-id"`, `"name": "msg-*"`, 1),
wantErr: "exact legacy flag",
},
{
name: "empty reason",
input: strings.Replace(validFlagMigrationManifestJSON, "Reviewed message identifier flag migration.", "", 1),
wantErr: "non-empty reason",
},
{
name: "invalid state",
input: strings.Replace(validFlagMigrationManifestJSON, `"state": "pending"`, `"state": "approved"`, 1),
wantErr: "invalid state",
},
{
name: "legacy removed",
input: strings.Replace(validFlagMigrationManifestJSON, `"after": {"present": true, "type": "string", "hidden": true, "scope": "local", "alias_of": "message-id"}`, `"after": {"present": false}`, 1),
wantErr: "legacy flag must remain present",
},
{
name: "canonical hidden",
input: strings.Replace(validFlagMigrationManifestJSON, `"after": {"present": true, "type": "string", "required": true, "scope": "local"}`, `"after": {"present": true, "type": "string", "required": true, "hidden": true, "scope": "local"}`, 1),
wantErr: "canonical flag must remain visible",
},
{
name: "canonical remains optional",
input: strings.Replace(
validFlagMigrationManifestJSON,
`"after": {"present": true, "type": "string", "required": true, "scope": "local"}`,
`"after": {"present": true, "type": "string", "scope": "local"}`,
1,
),
wantErr: "canonical flag must be required after migration",
},
{
name: "canonical was already required",
input: strings.Replace(
validFlagMigrationManifestJSON,
`"before": {"present": false}`,
`"before": {"present": true, "type": "string", "required": true, "scope": "local"}`,
1,
),
wantErr: "canonical flag must be absent or optional before migration",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
_, err := ReadFlagMigrationManifest(strings.NewReader(test.input))
if err == nil || !strings.Contains(err.Error(), test.wantErr) {
t.Fatalf("ReadFlagMigrationManifest() error = %v, want %q", err, test.wantErr)
}
})
}
}
func TestCrossPlatformCoverageFlagMigrationManifestRejectsDuplicateAndInexactContracts(t *testing.T) {
manifest, err := ReadFlagMigrationManifest(strings.NewReader(validFlagMigrationManifestJSON))
if err != nil {
t.Fatal(err)
}
duplicate := manifest
duplicate.Migrations = append(duplicate.Migrations, duplicate.Migrations[0])
if err := duplicate.Validate(); err == nil || !strings.Contains(err.Error(), "duplicates") {
t.Fatalf("duplicate migration error = %v", err)
}
whitespaceReason := manifest
whitespaceReason.Migrations = append([]FlagMigration(nil), manifest.Migrations...)
whitespaceReason.Migrations[0].Reason = " reviewed "
if err := whitespaceReason.Validate(); err == nil || !strings.Contains(err.Error(), "trimmed") {
t.Fatalf("whitespace reason error = %v", err)
}
canonicalDrift := manifest
canonicalDrift.Migrations = append([]FlagMigration(nil), manifest.Migrations...)
canonicalDrift.Migrations[0].Canonical.Before = FlagMigrationState{
Present: true,
Type: "string",
Scope: "local",
}
canonicalDrift.Migrations[0].Canonical.After.Type = "stringSlice"
if err := canonicalDrift.Validate(); err == nil || !strings.Contains(err.Error(), "canonical flag type") {
t.Fatalf("canonical type drift error = %v", err)
}
_, err = ReadFlagMigrationManifest(strings.NewReader(validFlagMigrationManifestJSON + ` {}`))
if err == nil || !strings.Contains(err.Error(), "trailing") {
t.Fatalf("trailing JSON error = %v", err)
}
}
func TestCrossPlatformCoverageAuthorizeFlagMigrationsReturnsExactBaseOwnedApproval(t *testing.T) {
pending := coverageManifest(FlagMigrationPending)
consumed := coverageManifest(FlagMigrationConsumed)
migration := pending.Migrations[0]
before := coverageMigrationSnapshot(migration, false, false)
after := coverageMigrationSnapshot(migration, true, false)
got, err := AuthorizeFlagMigrations(
after,
map[string]Snapshot{"merge-base": before, "stable": before},
pending,
consumed,
)
if err != nil {
t.Fatalf("AuthorizeFlagMigrations() error = %v", err)
}
if !reflect.DeepEqual(got, pending.Migrations) {
t.Fatalf("AuthorizeFlagMigrations() = %#v, want exact base-owned approval %#v", got, pending.Migrations)
}
}
+3
View File
@@ -117,5 +117,8 @@ func flagContractOtherwiseChanged(oldFlag, newFlag Flag) bool {
if !oldFlag.Hidden && newFlag.Hidden {
return true
}
if oldFlag.AliasOf != "" && newFlag.AliasOf != oldFlag.AliasOf {
return true
}
return false
}
+118 -9
View File
@@ -22,11 +22,16 @@ import (
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
)
const SchemaVersion = 1
const SchemaVersion = 2
// FlagAliasOfAnnotation records a framework-originated reviewed relationship
// between a retained compatibility flag and an exact canonical Cobra flag.
const FlagAliasOfAnnotation = runtimeannotate.AnnotationFlagAliasOf
var (
// Framework-owned command subtrees are deliberately excluded. They are
@@ -80,6 +85,12 @@ type Flag struct {
Required bool `json:"required"`
Hidden bool `json:"hidden"`
Deprecated string `json:"deprecated,omitempty"`
AliasOf string `json:"alias_of,omitempty"`
// Capture cannot return an error, so it preserves malformed or non-framework
// relation evidence here for Validate/Write to reject. Valid snapshots never
// serialize this implementation detail.
malformedAliasEvidence bool
}
// Capture walks root without rendering help or executing any command. Both
@@ -193,6 +204,9 @@ func (s Snapshot) Validate() error {
if err := validateFlags(command.Path, "inherited", command.InheritedFlags); err != nil {
return err
}
if err := validateFlagAliases(command); err != nil {
return err
}
}
return nil
}
@@ -206,21 +220,46 @@ func captureFlags(set *pflag.FlagSet) []Flag {
if flag == nil || excludedFlags[flag.Name] {
return
}
aliasOf, malformedAliasEvidence := captureFlagAliasOf(flag)
flags = append(flags, Flag{
Name: flag.Name,
Shorthand: flag.Shorthand,
Type: flag.Value.Type(),
Default: flag.DefValue,
NoOpt: flag.NoOptDefVal,
Required: isRequired(flag),
Hidden: flag.Hidden,
Deprecated: strings.TrimSpace(flag.Deprecated),
Name: flag.Name,
Shorthand: flag.Shorthand,
Type: flag.Value.Type(),
Default: flag.DefValue,
NoOpt: flag.NoOptDefVal,
Required: isRequired(flag),
Hidden: flag.Hidden,
Deprecated: strings.TrimSpace(flag.Deprecated),
AliasOf: aliasOf,
malformedAliasEvidence: malformedAliasEvidence,
})
})
sort.Slice(flags, func(i, j int) bool { return flags[i].Name < flags[j].Name })
return flags
}
func captureFlagAliasOf(flag *pflag.Flag) (string, bool) {
aliasOf, aliasMalformed := exactFlagAnnotation(flag, FlagAliasOfAnnotation)
origin, originMalformed := exactFlagAnnotation(flag, runtimeannotate.AnnotationFlagAliasOrigin)
if aliasOf == "" && origin == "" && !aliasMalformed && !originMalformed {
return "", false
}
malformed := aliasMalformed || originMalformed || aliasOf == "" ||
origin != runtimeannotate.FlagAliasOriginCorecmdV1
return aliasOf, malformed
}
func exactFlagAnnotation(flag *pflag.Flag, key string) (string, bool) {
values, exists := flag.Annotations[key]
if !exists {
return "", false
}
if len(values) != 1 || values[0] == "" || values[0] != strings.TrimSpace(values[0]) {
return "", true
}
return values[0], false
}
func isRequired(flag *pflag.Flag) bool {
for _, value := range flag.Annotations[cobra.BashCompOneRequiredFlag] {
if value == "true" {
@@ -268,3 +307,73 @@ func validateFlags(path, scope string, flags []Flag) error {
}
return nil
}
func validateFlagAliases(command Command) error {
flags := make(map[string]Flag, len(command.LocalFlags)+len(command.InheritedFlags))
ordered := make([]Flag, 0, len(command.LocalFlags)+len(command.InheritedFlags))
for _, flag := range command.LocalFlags {
flags[flag.Name] = flag
ordered = append(ordered, flag)
}
for _, flag := range command.InheritedFlags {
if _, exists := flags[flag.Name]; exists {
return fmt.Errorf("command %q contains flag %q in both local and inherited scopes", command.Path, flag.Name)
}
flags[flag.Name] = flag
ordered = append(ordered, flag)
}
for _, flag := range ordered {
if flag.malformedAliasEvidence {
return fmt.Errorf(
"command %q flag --%s must declare one exact %q value and framework origin %q",
command.Path,
flag.Name,
FlagAliasOfAnnotation,
runtimeannotate.FlagAliasOriginCorecmdV1,
)
}
if flag.AliasOf == "" {
continue
}
if !isExactFlagName(flag.AliasOf) {
return fmt.Errorf(
"command %q flag --%s has non-exact alias_of target %q",
command.Path,
flag.Name,
flag.AliasOf,
)
}
if flag.AliasOf == flag.Name {
return fmt.Errorf("command %q flag --%s cannot alias itself", command.Path, flag.Name)
}
target, exists := flags[flag.AliasOf]
if !exists {
return fmt.Errorf(
"command %q flag --%s aliases missing flag --%s",
command.Path,
flag.Name,
flag.AliasOf,
)
}
if target.AliasOf != "" {
return fmt.Errorf(
"command %q flag --%s aliases --%s, which is itself an alias",
command.Path,
flag.Name,
flag.AliasOf,
)
}
if flag.Type != target.Type {
return fmt.Errorf(
"command %q alias flags --%s and --%s have different types %q and %q",
command.Path,
flag.Name,
flag.AliasOf,
flag.Type,
target.Type,
)
}
}
return nil
}
+164 -1
View File
@@ -15,9 +15,12 @@ package interfacesnapshot
import (
"bytes"
"encoding/json"
"reflect"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/corecmd/runtimeannotate"
"github.com/spf13/cobra"
)
@@ -95,7 +98,7 @@ func TestCrossPlatformCoverageCaptureUsesStableNoiseRulesAndFlagScopes(t *testin
func TestCrossPlatformCoverageReadRejectsUnknownSnapshotFields(t *testing.T) {
input := bytes.NewBufferString(`{
"schema_version": 1,
"schema_version": 2,
"rules": {"excluded_command_subtrees": [], "excluded_flags": []},
"commands": [],
"future_field": true
@@ -105,6 +108,166 @@ func TestCrossPlatformCoverageReadRejectsUnknownSnapshotFields(t *testing.T) {
}
}
func TestCrossPlatformCoverageCaptureRoundTripsFrameworkFlagAlias(t *testing.T) {
if FlagAliasOfAnnotation != runtimeannotate.AnnotationFlagAliasOf {
t.Fatalf(
"flag alias annotation key drifted: snapshot=%q runtime=%q",
FlagAliasOfAnnotation,
runtimeannotate.AnnotationFlagAliasOf,
)
}
root := &cobra.Command{Use: "dws"}
leaf := &cobra.Command{Use: "send", Run: func(*cobra.Command, []string) {}}
corecmd.RegisterFlags(leaf, []corecmd.FlagSpec{{
Name: "message-id",
Usage: "message ID",
Aliases: []string{"open-conversation-id"},
}})
root.AddCommand(leaf)
snapshot := Capture(root)
flag := flagIndex(commandIndex(snapshot)["dws send"].LocalFlags)["open-conversation-id"]
if flag.AliasOf != "message-id" {
t.Fatalf("captured alias_of = %q, want message-id", flag.AliasOf)
}
var encoded bytes.Buffer
if err := Write(&encoded, snapshot); err != nil {
t.Fatalf("Write: %v", err)
}
decoded, err := Read(bytes.NewReader(encoded.Bytes()))
if err != nil {
t.Fatalf("Read: %v", err)
}
decodedFlag := flagIndex(commandIndex(decoded)["dws send"].LocalFlags)["open-conversation-id"]
if decodedFlag.AliasOf != "message-id" {
t.Fatalf("round-tripped alias_of = %q, want message-id", decodedFlag.AliasOf)
}
}
func TestCrossPlatformCoverageCaptureRejectsUntrustedFlagAliasEvidence(t *testing.T) {
for _, origin := range []string{"", "handwritten.alias.v1"} {
t.Run(origin, func(t *testing.T) {
root := &cobra.Command{Use: "dws"}
leaf := &cobra.Command{Use: "send", Run: func(*cobra.Command, []string) {}}
leaf.Flags().String("message-id", "", "canonical message ID")
leaf.Flags().String("legacy-id", "", "legacy message ID")
legacy := leaf.Flags().Lookup("legacy-id")
legacy.Annotations = map[string][]string{
FlagAliasOfAnnotation: {"message-id"},
}
if origin != "" {
legacy.Annotations[runtimeannotate.AnnotationFlagAliasOrigin] = []string{origin}
}
root.AddCommand(leaf)
if err := Capture(root).Validate(); err == nil {
t.Fatal("Validate accepted alias evidence outside corecmd FlagSpec.Aliases")
}
})
}
}
func TestCrossPlatformCoverageCaptureRejectsMultipleNonEmptyFlagAliases(t *testing.T) {
root := &cobra.Command{Use: "dws"}
leaf := &cobra.Command{Use: "send", Run: func(*cobra.Command, []string) {}}
leaf.Flags().String("message-id", "", "canonical message ID")
leaf.Flags().String("other-id", "", "other ID")
leaf.Flags().String("legacy-id", "", "legacy ID")
legacy := leaf.Flags().Lookup("legacy-id")
legacy.Annotations = map[string][]string{
FlagAliasOfAnnotation: {"message-id", "other-id"},
}
root.AddCommand(leaf)
snapshot := Capture(root)
if err := snapshot.Validate(); err == nil {
t.Fatal("Validate accepted multiple non-empty alias annotation values")
}
var encoded bytes.Buffer
if err := Write(&encoded, snapshot); err == nil {
t.Fatal("Write accepted multiple non-empty alias annotation values")
}
}
func TestCrossPlatformCoverageValidateRejectsMalformedFlagAliasState(t *testing.T) {
tests := []struct {
name string
aliasOf string
}{
{name: "self", aliasOf: "legacy-id"},
{name: "leading dashes", aliasOf: "--message-id"},
{name: "non exact", aliasOf: "message id"},
{name: "missing target", aliasOf: "missing-id"},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
snapshot := testSnapshot(
testCommand("dws"),
testCommandWithFlagScopes(
"dws send",
[]Flag{
{Name: "legacy-id", Type: "string", AliasOf: test.aliasOf},
{Name: "message-id", Type: "string"},
},
nil,
),
)
if err := snapshot.Validate(); err == nil {
t.Fatalf("Validate accepted alias_of %q", test.aliasOf)
}
encoded, err := json.Marshal(snapshot)
if err != nil {
t.Fatalf("json.Marshal: %v", err)
}
if _, err := Read(bytes.NewReader(encoded)); err == nil {
t.Fatalf("Read accepted alias_of %q", test.aliasOf)
}
})
}
}
func TestCrossPlatformCoverageValidateRejectsFlagAliasChainTypeAndScopeConflicts(t *testing.T) {
tests := []struct {
name string
local []Flag
inherited []Flag
}{
{
name: "alias chain",
local: []Flag{
{Name: "legacy-id", Type: "string", AliasOf: "middle-id"},
{Name: "middle-id", Type: "string", AliasOf: "message-id"},
{Name: "message-id", Type: "string"},
},
},
{
name: "different types",
local: []Flag{
{Name: "legacy-id", Type: "string", AliasOf: "message-id"},
{Name: "message-id", Type: "int"},
},
},
{
name: "same flag in local and inherited scopes",
local: []Flag{{Name: "legacy-id", Type: "string"}},
inherited: []Flag{{Name: "legacy-id", Type: "string"}},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
snapshot := testSnapshot(
testCommand("dws"),
testCommandWithFlagScopes("dws send", test.local, test.inherited),
)
if err := snapshot.Validate(); err == nil {
t.Fatalf("Validate accepted invalid alias contract: %#v", snapshot.Commands[1])
}
})
}
}
func TestCrossPlatformCoverageCompareBlocksCandidateSiblingAliasCollision(t *testing.T) {
base := testSnapshot(testCommand("dws"))
current := testSnapshot(
+1 -1
View File
@@ -19,7 +19,7 @@ Script groups:
- Product convenience installers: `./scripts/install-devapp.sh`, `./scripts/install-devapp.ps1`, `./scripts/install-event.sh`
- CI package plan: `./scripts/ci/test-packages.sh`
- Dev helpers: `./scripts/dev/build.sh`, `./scripts/dev/lint.sh`, `./scripts/dev/ci-local.sh`, `./scripts/dev/run-mock-e2e.sh`, `./scripts/dev/coverage.sh`
- Policy checks: `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-command-surface.sh`, `./scripts/policy/check-command-compatibility.sh --base-ref <main-ref> --stable-ref <latest-GA-tag>`, `./scripts/policy/check-schema-catalog.sh`, `./scripts/policy/check-schema-binary.sh`, `./scripts/policy/check-open-source-assets.sh`
- Policy checks: `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-command-surface.sh`, `./scripts/policy/check-command-compatibility.sh --base-ref <main-ref> --stable-ref <latest-GA-tag> --candidate-ref <candidate-sha>`, `./scripts/policy/check-schema-catalog.sh`, `./scripts/policy/check-schema-binary.sh`, `./scripts/policy/check-open-source-assets.sh`
- Policy runtime files: set `DWS_POLICY_TMPDIR` to override the default `.worktrees/policy-tmp` workspace
- Release entrypoint and contract: `./scripts/release/release.sh`, `./scripts/release/release-contract.sh`; operator guide: [`docs/releasing.md`](../docs/releasing.md)
- Release packaging helpers: `./scripts/release/post-goreleaser.sh`, `./scripts/release/stage-npm-package.sh`, `./scripts/release/pack-npm-package.sh`, `./scripts/release/verify-delivered-stable.sh`, `./scripts/release/verify-release-artifacts.sh`, `./scripts/release/verify-github-release-assets.sh`, `./scripts/release/verify-github-tag-authority.sh`, `./scripts/release/verify-package-managers.sh`, `./scripts/release/publish-homebrew-formula.sh`
@@ -1,15 +1,16 @@
#!/bin/sh
set -eu
# Compare the candidate command tree against a Git-owned historical source
# revision. CI invokes this for both the PR merge-base and latest stable tag; a
# fixture from the candidate branch is never used as authority.
# Compatibility decisions are centralized in check-command-compatibility.sh.
# This historical entry point remains as the Makefile/user-facing wrapper.
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
BASE_REF=""
STABLE_REF=""
CANDIDATE_REF="HEAD"
usage() {
printf '%s\n' "usage: $0 --base-ref <ref>" >&2
printf '%s\n' "usage: $0 --base-ref <ref> [--stable-ref <ref>] [--candidate-ref <ref>]" >&2
}
while [ "$#" -gt 0 ]; do
@@ -19,6 +20,16 @@ while [ "$#" -gt 0 ]; do
BASE_REF="$2"
shift 2
;;
--stable-ref)
[ "$#" -ge 2 ] || { usage; exit 2; }
STABLE_REF="$2"
shift 2
;;
--candidate-ref)
[ "$#" -ge 2 ] || { usage; exit 2; }
CANDIDATE_REF="$2"
shift 2
;;
-h|--help)
usage
exit 0
@@ -32,63 +43,30 @@ while [ "$#" -gt 0 ]; do
done
[ -n "$BASE_REF" ] || { usage; exit 2; }
cd "$ROOT"
. "$ROOT/scripts/policy/policy-runtime.sh"
policy_prepare_runtime "$ROOT"
. "$ROOT/scripts/release/release-lib.sh"
git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null || {
printf 'error: interface authority is not available locally: %s\n' "$BASE_REF" >&2
exit 2
}
TMP_ROOT="$(policy_runtime_mktemp_dir dws-interface-authority)"
BASE_WORKTREE="$TMP_ROOT/base-worktree"
BASELINE="$TMP_ROOT/merge-base.txt"
CANDIDATE_BIN="$TMP_ROOT/interface-baseline"
CANDIDATE_HOME="$TMP_ROOT/candidate-home"
if [ -z "$STABLE_REF" ]; then
for tag in $(git tag --merged "$BASE_REF" --list 'v*' --sort=-version:refname); do
release_is_stable_version "$tag" || continue
if git rev-parse --verify --quiet "refs/tags/withdrawn/$tag" >/dev/null; then
continue
fi
STABLE_REF="$tag"
break
done
fi
if [ -z "$STABLE_REF" ]; then
printf 'error: no stable release tag is reachable from interface authority %s\n' "$BASE_REF" >&2
exit 2
fi
cleanup() {
git -C "$ROOT" worktree remove --force "$BASE_WORKTREE" >/dev/null 2>&1 || true
rm -rf "$TMP_ROOT"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
install_snapshot_helper() {
worktree="$1"
helper="$worktree/scripts/policy/interface-baseline"
# Use one contract format on both sides while constructing the command tree
# from the authoritative historical source revision.
rm -rf "$helper"
mkdir -p "$helper"
cp -R "$ROOT/scripts/policy/interface-baseline/." "$helper/"
}
generate_reference() {
ref="$1"
worktree="$2"
output="$3"
home="$4"
bin="$TMP_ROOT/reference-$(basename "$worktree")"
git -C "$ROOT" worktree add --detach "$worktree" "$ref" >/dev/null
install_snapshot_helper "$worktree"
mkdir -p "$home"
(
cd "$worktree"
go build -o "$bin" ./scripts/policy/interface-baseline
HOME="$home" DWS_LANG=zh "$bin" >"$output"
)
}
generate_reference "$BASE_REF" "$BASE_WORKTREE" "$BASELINE" "$TMP_ROOT/base-home"
mkdir -p "$CANDIDATE_HOME"
go build -o "$CANDIDATE_BIN" ./scripts/policy/interface-baseline
printf 'checking candidate against authoritative interface ref %s\n' "$BASE_REF"
HOME="$CANDIDATE_HOME" DWS_LANG=zh "$CANDIDATE_BIN" --check "$BASELINE"
exec "$ROOT/scripts/policy/check-command-compatibility.sh" \
--base-ref "$BASE_REF" \
--stable-ref "$STABLE_REF" \
--candidate-ref "$CANDIDATE_REF"
@@ -1,16 +1,23 @@
#!/bin/sh
set -eu
# Compare the candidate's complete Schema contract with the Git-owned PR
# merge-base (or previous main SHA on push). The candidate branch cannot
# weaken this authority by editing a checked-in fixture.
# Compare the committed candidate's complete Schema contract independently with
# the Git-owned PR merge-base (or previous main SHA on push) and the latest
# reachable stable GA. The merge-base owns the Schema checker and, after
# bootstrap, the approved flag-migration ledger. Candidate code is lifecycle
# input only and cannot replace either authority.
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
BIN="${DWS_BIN:-$ROOT/dws}"
BASE_REF=""
STABLE_REF=""
CANDIDATE_REF="HEAD"
SCHEMA_CHECKER_REL="scripts/policy/schema-compat/main.go"
MIGRATION_MANIFEST_REL="scripts/policy/interface-migrations/approved-flag-migrations-v1.json"
MIGRATIONS_REL="internal/interfacesnapshot/migrations.go"
ALIAS_CONTRACT_REL="internal/corecmd/runtimeannotate/interface_alias.go"
usage() {
printf '%s\n' "usage: $0 --base-ref <ref>" >&2
printf '%s\n' "usage: $0 --base-ref <ref> [--stable-ref <ref>] [--candidate-ref <ref>]" >&2
}
while [ "$#" -gt 0 ]; do
@@ -20,6 +27,16 @@ while [ "$#" -gt 0 ]; do
BASE_REF="$2"
shift 2
;;
--stable-ref)
[ "$#" -ge 2 ] || { usage; exit 2; }
STABLE_REF="$2"
shift 2
;;
--candidate-ref)
[ "$#" -ge 2 ] || { usage; exit 2; }
CANDIDATE_REF="$2"
shift 2
;;
-h|--help)
usage
exit 0
@@ -36,27 +53,62 @@ done
cd "$ROOT"
. "$ROOT/scripts/policy/policy-runtime.sh"
. "$ROOT/scripts/release/release-lib.sh"
policy_prepare_runtime "$ROOT"
git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null || {
BASE_COMMIT="$(git rev-parse --verify "${BASE_REF}^{commit}")" || {
printf 'error: Schema authority is not available locally: %s\n' "$BASE_REF" >&2
exit 2
}
if [ ! -x "$BIN" ]; then
printf 'error: dws binary not found at %s (run make build first)\n' "$BIN" >&2
CANDIDATE_COMMIT="$(git rev-parse --verify "${CANDIDATE_REF}^{commit}")" || {
printf 'error: Schema candidate is not available locally: %s\n' "$CANDIDATE_REF" >&2
exit 2
}
EXPECTED_STABLE_REF=""
for tag in $(git tag --merged "$BASE_REF" --list 'v*' --sort=-version:refname); do
release_is_stable_version "$tag" || continue
if git rev-parse --verify --quiet "refs/tags/withdrawn/$tag" >/dev/null; then
continue
fi
EXPECTED_STABLE_REF="$tag"
break
done
if [ -z "$EXPECTED_STABLE_REF" ]; then
printf 'error: no stable release tag is reachable from Schema authority %s\n' "$BASE_REF" >&2
exit 2
fi
if [ -z "$STABLE_REF" ]; then
STABLE_REF="$EXPECTED_STABLE_REF"
fi
STABLE_COMMIT="$(git rev-parse --verify "${STABLE_REF}^{commit}")" || {
printf 'error: Schema stable authority is not available locally: %s\n' "$STABLE_REF" >&2
exit 2
}
if [ "$(git rev-parse "${STABLE_REF}^{commit}")" != "$(git rev-parse "${EXPECTED_STABLE_REF}^{commit}")" ]; then
printf 'error: stable ref %s is not the expected highest GA tag %s reachable from %s\n' \
"$STABLE_REF" "$EXPECTED_STABLE_REF" "$BASE_REF" >&2
exit 2
fi
TMP_ROOT="$(policy_runtime_mktemp_dir dws-schema-authority)"
BASE_WORKTREE="$TMP_ROOT/base-worktree"
BASE_BIN="$TMP_ROOT/base-dws"
BASE_RAW="$TMP_ROOT/base-schema.json"
BASELINE="$TMP_ROOT/base-contract.json"
CANDIDATE_RAW="$TMP_ROOT/candidate-schema.json"
CHECKER="$TMP_ROOT/schema-compat"
STABLE_WORKTREE="$TMP_ROOT/stable-worktree"
CANDIDATE_WORKTREE="$TMP_ROOT/candidate-worktree"
BASE_WORKTREE_ADDED=false
STABLE_WORKTREE_ADDED=false
CANDIDATE_WORKTREE_ADDED=false
cleanup() {
git -C "$ROOT" worktree remove --force "$BASE_WORKTREE" >/dev/null 2>&1 || true
if [ "$CANDIDATE_WORKTREE_ADDED" = true ]; then
git -C "$ROOT" worktree remove --force "$CANDIDATE_WORKTREE" >/dev/null 2>&1 || true
fi
if [ "$STABLE_WORKTREE_ADDED" = true ]; then
git -C "$ROOT" worktree remove --force "$STABLE_WORKTREE" >/dev/null 2>&1 || true
fi
if [ "$BASE_WORKTREE_ADDED" = true ]; then
git -C "$ROOT" worktree remove --force "$BASE_WORKTREE" >/dev/null 2>&1 || true
fi
rm -rf "$TMP_ROOT"
}
trap cleanup EXIT
@@ -64,19 +116,237 @@ trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
git -C "$ROOT" worktree add --detach "$BASE_WORKTREE" "$BASE_REF" >/dev/null
git -C "$ROOT" worktree add --detach "$BASE_WORKTREE" "$BASE_COMMIT" >/dev/null
BASE_WORKTREE_ADDED=true
git -C "$ROOT" worktree add --detach "$STABLE_WORKTREE" "$STABLE_COMMIT" >/dev/null
STABLE_WORKTREE_ADDED=true
git -C "$ROOT" worktree add --detach "$CANDIDATE_WORKTREE" "$CANDIDATE_COMMIT" >/dev/null
CANDIDATE_WORKTREE_ADDED=true
commit_path_exists() {
tree_commit="$1"
tree_path="$2"
tree_entry="$(git -C "$ROOT" ls-tree "$tree_commit" -- "$tree_path")"
[ -n "$tree_entry" ]
}
commit_path_is_regular_file() {
tree_commit="$1"
tree_path="$2"
tree_entry="$(git -C "$ROOT" ls-tree "$tree_commit" -- "$tree_path")"
[ -n "$tree_entry" ] || return 1
set -- $tree_entry
[ "$#" -eq 4 ] || return 1
case "$1" in
100644|100755) ;;
*) return 1 ;;
esac
[ "$2" = blob ] && [ "$4" = "$tree_path" ]
}
has_modern_interface_helper() {
governance_commit="$1"
commit_path_is_regular_file "$governance_commit" cmd/interface-snapshot/main.go &&
commit_path_is_regular_file "$governance_commit" internal/interfacesnapshot/snapshot.go &&
commit_path_is_regular_file "$governance_commit" internal/interfacesnapshot/compare.go
}
has_complete_schema_migration_governance() {
governance_commit="$1"
commit_path_is_regular_file "$governance_commit" "$SCHEMA_CHECKER_REL" &&
has_modern_interface_helper "$governance_commit" &&
commit_path_is_regular_file "$governance_commit" "$MIGRATIONS_REL" &&
commit_path_is_regular_file "$governance_commit" "$ALIAS_CONTRACT_REL" &&
commit_path_is_regular_file "$governance_commit" "$MIGRATION_MANIFEST_REL"
}
has_any_schema_migration_governance_artifact() {
governance_commit="$1"
for relative_path in \
"$MIGRATIONS_REL" \
"$ALIAS_CONTRACT_REL" \
"$MIGRATION_MANIFEST_REL"; do
if commit_path_exists "$governance_commit" "$relative_path"; then
return 0
fi
done
return 1
}
check_candidate_alias_source_policy() {
source_policy_failed=false
for token in \
dws.compat.alias_of \
dws.compat.alias_origin \
corecmd.flag_spec_aliases.v1 \
AnnotationFlagAliasOf \
AnnotationFlagAliasOrigin \
FlagAliasOriginCorecmdV1; do
matches="$(git -C "$CANDIDATE_WORKTREE" grep -l -F -e "$token" -- '*.go' || true)"
[ -z "$matches" ] && continue
while IFS= read -r relative_path; do
[ -n "$relative_path" ] || continue
case "$relative_path" in
*_test.go)
continue
;;
internal/corecmd/corecmd.go|\
internal/corecmd/runtimeannotate/interface_alias.go|\
internal/interfacesnapshot/snapshot.go)
;;
*)
if [ "$source_policy_failed" = false ]; then
printf 'error: candidate production source may not forge framework-owned flag alias evidence:\n' >&2
fi
printf ' %s: protected alias evidence token %s\n' "$relative_path" "$token" >&2
source_policy_failed=true
;;
esac
done <<EOF
$matches
EOF
done
if [ "$source_policy_failed" = true ]; then
exit 2
fi
}
if ! commit_path_is_regular_file "$BASE_COMMIT" "$SCHEMA_CHECKER_REL"; then
printf 'error: merge-base lacks the base-owned Schema checker: %s\n' "$BASE_REF" >&2
exit 2
fi
if ! has_modern_interface_helper "$BASE_COMMIT"; then
printf 'error: merge-base lacks the modern interface snapshot helper required for Schema governance: %s\n' "$BASE_REF" >&2
exit 2
fi
if ! has_complete_schema_migration_governance "$CANDIDATE_COMMIT"; then
printf 'error: candidate must preserve the complete Schema flag migration governance artifact set at %s\n' \
"$CANDIDATE_COMMIT" >&2
exit 2
fi
check_candidate_alias_source_policy
EMPTY_MANIFEST="$TMP_ROOT/empty-migrations.json"
printf '%s\n' '{"version":1,"migrations":[]}' >"$EMPTY_MANIFEST"
USE_MIGRATION_GOVERNANCE=false
if has_complete_schema_migration_governance "$BASE_COMMIT"; then
USE_MIGRATION_GOVERNANCE=true
APPROVED_MANIFEST="$BASE_WORKTREE/$MIGRATION_MANIFEST_REL"
CANDIDATE_MANIFEST="$CANDIDATE_WORKTREE/$MIGRATION_MANIFEST_REL"
else
# One-time bootstrap for the governance PR itself. The old base checker owns
# the decision and receives no flags it does not understand. With no base
# approval authority, only the exact canonical empty candidate ledger is
# admissible.
if has_any_schema_migration_governance_artifact "$BASE_COMMIT"; then
printf 'error: merge-base contains an incomplete Schema flag migration governance artifact set: %s\n' \
"$BASE_REF" >&2
exit 2
fi
BOOTSTRAP_MANIFEST="$CANDIDATE_WORKTREE/$MIGRATION_MANIFEST_REL"
if ! cmp -s "$BOOTSTRAP_MANIFEST" "$EMPTY_MANIFEST"; then
printf 'error: initial Schema flag migration governance requires the canonical empty manifest: %s\n' \
"$BOOTSTRAP_MANIFEST" >&2
exit 2
fi
fi
BASE_BIN="$TMP_ROOT/base-dws"
STABLE_BIN="$TMP_ROOT/stable-dws"
CANDIDATE_BIN="$TMP_ROOT/candidate-dws"
CHECKER="$TMP_ROOT/schema-compat"
BASE_RAW="$TMP_ROOT/base-schema.json"
BASELINE="$TMP_ROOT/base-contract.json"
STABLE_RAW="$TMP_ROOT/stable-schema.json"
STABLE_BASELINE="$TMP_ROOT/stable-contract.json"
CANDIDATE_RAW="$TMP_ROOT/candidate-schema.json"
(
cd "$BASE_WORKTREE"
go build -o "$BASE_BIN" ./cmd
go build -o "$CHECKER" ./scripts/policy/schema-compat
)
(
cd "$STABLE_WORKTREE"
go build -o "$STABLE_BIN" ./cmd
)
(
cd "$CANDIDATE_WORKTREE"
go build -o "$CANDIDATE_BIN" ./cmd
)
go build -o "$CHECKER" ./scripts/policy/schema-compat
mkdir -p "$TMP_ROOT/base-home" "$TMP_ROOT/candidate-home"
mkdir -p "$TMP_ROOT/base-home" "$TMP_ROOT/stable-home" "$TMP_ROOT/candidate-home"
HOME="$TMP_ROOT/base-home" DWS_LANG=zh \
"$BASE_BIN" schema --all --format json >"$BASE_RAW"
HOME="$TMP_ROOT/stable-home" DWS_LANG=zh \
"$STABLE_BIN" schema --all --format json >"$STABLE_RAW"
HOME="$TMP_ROOT/candidate-home" DWS_LANG=zh \
"$BIN" schema --all --format json >"$CANDIDATE_RAW"
"$CANDIDATE_BIN" schema --all --format json >"$CANDIDATE_RAW"
"$CHECKER" --normalize "$BASE_RAW" >"$BASELINE"
printf 'checking complete Schema contract against PR merge-base %s\n' "$BASE_REF"
"$CHECKER" --check "$BASELINE" --current "$CANDIDATE_RAW"
"$CHECKER" --normalize "$STABLE_RAW" >"$STABLE_BASELINE"
check_schema_contract() {
historical_kind="$1"
historical_ref="$2"
historical_baseline="$3"
shift 3
printf 'checking complete Schema contract against %s %s\n' \
"$historical_kind" "$historical_ref"
"$CHECKER" --check "$historical_baseline" --current "$CANDIDATE_RAW" "$@"
}
if [ "$USE_MIGRATION_GOVERNANCE" = false ]; then
check_schema_contract "PR merge-base" "$BASE_REF" "$BASELINE"
check_schema_contract "stable" "$STABLE_REF" "$STABLE_BASELINE"
exit 0
fi
install_authority_interface_helper() {
worktree="$1"
for helper_path in cmd/interface-snapshot internal/interfacesnapshot; do
rm -rf "$worktree/$helper_path"
mkdir -p "$worktree/$helper_path"
cp -R "$BASE_WORKTREE/$helper_path/." "$worktree/$helper_path/"
done
mkdir -p "$worktree/$(dirname "$ALIAS_CONTRACT_REL")"
rm -f "$worktree/$ALIAS_CONTRACT_REL"
cp "$BASE_WORKTREE/$ALIAS_CONTRACT_REL" "$worktree/$ALIAS_CONTRACT_REL"
}
# The candidate and historical stable trees provide only the command surface.
# The merge-base owns the snapshot generator, comparator, and alias contract.
install_authority_interface_helper "$CANDIDATE_WORKTREE"
install_authority_interface_helper "$STABLE_WORKTREE"
generate_interface_snapshot() {
worktree="$1"
output="$2"
(
cd "$worktree"
go run ./cmd/interface-snapshot generate --output "$output"
)
}
CURRENT_INTERFACE_SNAPSHOT="$TMP_ROOT/candidate-interface-snapshot.json"
BASE_INTERFACE_SNAPSHOT="$TMP_ROOT/base-interface-snapshot.json"
STABLE_INTERFACE_SNAPSHOT="$TMP_ROOT/stable-interface-snapshot.json"
generate_interface_snapshot "$CANDIDATE_WORKTREE" "$CURRENT_INTERFACE_SNAPSHOT"
generate_interface_snapshot "$BASE_WORKTREE" "$BASE_INTERFACE_SNAPSHOT"
generate_interface_snapshot "$STABLE_WORKTREE" "$STABLE_INTERFACE_SNAPSHOT"
check_schema_contract "PR merge-base" "$BASE_REF" "$BASELINE" \
--approved-flag-migrations "$APPROVED_MANIFEST" \
--candidate-flag-migrations "$CANDIDATE_MANIFEST" \
--migration-current-snapshot "$CURRENT_INTERFACE_SNAPSHOT" \
--migration-base-snapshot "$BASE_INTERFACE_SNAPSHOT" \
--migration-stable-snapshot "$STABLE_INTERFACE_SNAPSHOT"
check_schema_contract "stable" "$STABLE_REF" "$STABLE_BASELINE" \
--approved-flag-migrations "$APPROVED_MANIFEST" \
--candidate-flag-migrations "$CANDIDATE_MANIFEST" \
--migration-current-snapshot "$CURRENT_INTERFACE_SNAPSHOT" \
--migration-base-snapshot "$BASE_INTERFACE_SNAPSHOT" \
--migration-stable-snapshot "$STABLE_INTERFACE_SNAPSHOT"
+228 -49
View File
@@ -1,23 +1,21 @@
#!/bin/sh
set -eu
# Compare the current Cobra command surface with two local Git revisions:
# the PR merge-base/main reference and the latest stable GA tag. The caller must
# pass the highest non-prerelease SemVer tag (excluding beta/rc tags). Tag
# governance treats that tag as proof of a successfully published GA release;
# release automation must not leave a GA tag behind when publication fails.
#
# The script never fetches refs or snapshots from the network. With checkout
# fetch-depth=0, both references are materialized in temporary worktrees. A
# reference uses its own snapshot helper when available; older revisions are
# bootstrapped with the candidate helper.
# Compare the committed candidate Cobra surface with the PR merge-base and the
# latest stable GA tag. Once the merge-base contains the migration governance
# helper, that revision owns the generator, comparator, and approved manifest.
# The candidate manifest is lifecycle input only and cannot authorize a change
# introduced by the same PR.
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
BASE_REF=""
STABLE_REF=""
CANDIDATE_REF="HEAD"
MIGRATION_MANIFEST_REL="scripts/policy/interface-migrations/approved-flag-migrations-v1.json"
ALIAS_CONTRACT_REL="internal/corecmd/runtimeannotate/interface_alias.go"
usage() {
printf '%s\n' "usage: $0 --base-ref <ref> --stable-ref <ref>" >&2
printf '%s\n' "usage: $0 --base-ref <ref> --stable-ref <ref> [--candidate-ref <ref>]" >&2
}
while [ "$#" -gt 0 ]; do
@@ -32,6 +30,11 @@ while [ "$#" -gt 0 ]; do
STABLE_REF="$2"
shift 2
;;
--candidate-ref)
[ "$#" -ge 2 ] || { usage; exit 2; }
CANDIDATE_REF="$2"
shift 2
;;
-h|--help)
usage
exit 0
@@ -47,20 +50,51 @@ done
[ -n "$BASE_REF" ] && [ -n "$STABLE_REF" ] || { usage; exit 2; }
cd "$ROOT"
git rev-parse --verify --quiet "${BASE_REF}^{commit}" >/dev/null || {
. "$ROOT/scripts/policy/policy-runtime.sh"
. "$ROOT/scripts/release/release-lib.sh"
policy_prepare_runtime "$ROOT"
BASE_COMMIT="$(git rev-parse --verify "${BASE_REF}^{commit}")" || {
printf 'base ref is not available locally: %s\n' "$BASE_REF" >&2
exit 2
}
git rev-parse --verify --quiet "${STABLE_REF}^{commit}" >/dev/null || {
STABLE_COMMIT="$(git rev-parse --verify "${STABLE_REF}^{commit}")" || {
printf 'stable ref is not available locally: %s\n' "$STABLE_REF" >&2
exit 2
}
CANDIDATE_COMMIT="$(git rev-parse --verify "${CANDIDATE_REF}^{commit}")" || {
printf 'candidate ref is not available locally: %s\n' "$CANDIDATE_REF" >&2
exit 2
}
EXPECTED_STABLE_REF=""
for tag in $(git tag --merged "$BASE_REF" --list 'v*' --sort=-version:refname); do
release_is_stable_version "$tag" || continue
if git rev-parse --verify --quiet "refs/tags/withdrawn/$tag" >/dev/null; then
continue
fi
EXPECTED_STABLE_REF="$tag"
break
done
if [ -z "$EXPECTED_STABLE_REF" ]; then
printf 'no stable GA tag is reachable from compatibility base %s\n' "$BASE_REF" >&2
exit 2
fi
if [ "$(git rev-parse "${STABLE_REF}^{commit}")" != "$(git rev-parse "${EXPECTED_STABLE_REF}^{commit}")" ]; then
printf 'stable ref %s is not the expected highest GA tag %s reachable from %s\n' \
"$STABLE_REF" "$EXPECTED_STABLE_REF" "$BASE_REF" >&2
exit 2
fi
TMP_ROOT="$(mktemp -d "${TMPDIR:-/tmp}/dws-command-compat.XXXXXX")"
TMP_ROOT="$(policy_runtime_mktemp_dir dws-command-compat)"
BASE_WORKTREE="$TMP_ROOT/base-worktree"
STABLE_WORKTREE="$TMP_ROOT/stable-worktree"
CANDIDATE_WORKTREE="$TMP_ROOT/candidate-worktree"
CANDIDATE_WORKTREE_ADDED=false
cleanup() {
if [ "$CANDIDATE_WORKTREE_ADDED" = true ]; then
git -C "$ROOT" worktree remove --force "$CANDIDATE_WORKTREE" >/dev/null 2>&1 || true
fi
git -C "$ROOT" worktree remove --force "$BASE_WORKTREE" >/dev/null 2>&1 || true
git -C "$ROOT" worktree remove --force "$STABLE_WORKTREE" >/dev/null 2>&1 || true
rm -rf "$TMP_ROOT"
@@ -70,38 +104,168 @@ trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
ensure_snapshot_helper() {
worktree="$1"
git -C "$ROOT" worktree add --detach "$BASE_WORKTREE" "$BASE_COMMIT" >/dev/null
git -C "$ROOT" worktree add --detach "$STABLE_WORKTREE" "$STABLE_COMMIT" >/dev/null
git -C "$ROOT" worktree add --detach "$CANDIDATE_WORKTREE" "$CANDIDATE_COMMIT" >/dev/null
CANDIDATE_WORKTREE_ADDED=true
# Once a baseline contains the helper, use that revision's own capture
# rules. This makes a rule/filter change in the candidate visible as a
# snapshot_rules_changed failure instead of applying the new exclusion to
# both sides and accidentally hiding a removed command. Older revisions
# created before this helper existed are bootstrapped with the candidate
# implementation.
if [ -f "$worktree/cmd/interface-snapshot/main.go" ] && \
[ -f "$worktree/internal/interfacesnapshot/snapshot.go" ] && \
[ -f "$worktree/internal/interfacesnapshot/compare.go" ]; then
return
fi
if [ -e "$worktree/cmd/interface-snapshot" ] || \
[ -e "$worktree/internal/interfacesnapshot" ]; then
printf 'baseline contains an incomplete interface snapshot helper: %s\n' "$worktree" >&2
exit 2
fi
mkdir -p "$worktree/cmd/interface-snapshot" "$worktree/internal/interfacesnapshot"
cp -R "$ROOT/cmd/interface-snapshot/." "$worktree/cmd/interface-snapshot/"
cp -R "$ROOT/internal/interfacesnapshot/." "$worktree/internal/interfacesnapshot/"
commit_path_exists() {
tree_commit="$1"
tree_path="$2"
tree_entry="$(git -C "$ROOT" ls-tree "$tree_commit" -- "$tree_path")"
[ -n "$tree_entry" ]
}
generate_ref_snapshot() {
ref="$1"
worktree="$2"
output="$3"
commit_path_is_regular_file() {
tree_commit="$1"
tree_path="$2"
tree_entry="$(git -C "$ROOT" ls-tree "$tree_commit" -- "$tree_path")"
[ -n "$tree_entry" ] || return 1
set -- $tree_entry
[ "$#" -eq 4 ] || return 1
case "$1" in
100644|100755) ;;
*) return 1 ;;
esac
[ "$2" = blob ] && [ "$4" = "$tree_path" ]
}
git -C "$ROOT" worktree add --detach "$worktree" "$ref" >/dev/null
ensure_snapshot_helper "$worktree"
has_modern_interface_helper() {
governance_commit="$1"
commit_path_is_regular_file "$governance_commit" cmd/interface-snapshot/main.go &&
commit_path_is_regular_file "$governance_commit" internal/interfacesnapshot/snapshot.go &&
commit_path_is_regular_file "$governance_commit" internal/interfacesnapshot/compare.go
}
has_complete_migration_governance() {
governance_commit="$1"
has_modern_interface_helper "$governance_commit" &&
commit_path_is_regular_file "$governance_commit" internal/interfacesnapshot/migrations.go &&
commit_path_is_regular_file "$governance_commit" "$ALIAS_CONTRACT_REL" &&
commit_path_is_regular_file "$governance_commit" "$MIGRATION_MANIFEST_REL"
}
has_any_migration_governance_artifact() {
governance_commit="$1"
for relative_path in \
internal/interfacesnapshot/migrations.go \
"$ALIAS_CONTRACT_REL" \
"$MIGRATION_MANIFEST_REL"; do
if commit_path_exists "$governance_commit" "$relative_path"; then
return 0
fi
done
return 1
}
require_complete_candidate_governance() {
if ! has_complete_migration_governance "$CANDIDATE_COMMIT"; then
printf 'candidate must preserve the complete flag migration governance artifact set at %s\n' \
"$CANDIDATE_COMMIT" >&2
exit 2
fi
}
check_candidate_alias_source_policy() {
source_policy_failed=false
for token in \
dws.compat.alias_of \
dws.compat.alias_origin \
corecmd.flag_spec_aliases.v1 \
AnnotationFlagAliasOf \
AnnotationFlagAliasOrigin \
FlagAliasOriginCorecmdV1; do
matches="$(git -C "$CANDIDATE_WORKTREE" grep -l -F -e "$token" -- '*.go' || true)"
[ -z "$matches" ] && continue
while IFS= read -r relative_path; do
[ -n "$relative_path" ] || continue
case "$relative_path" in
*_test.go)
continue
;;
internal/corecmd/corecmd.go|\
internal/corecmd/runtimeannotate/interface_alias.go|\
internal/interfacesnapshot/snapshot.go)
;;
*)
if [ "$source_policy_failed" = false ]; then
printf 'candidate production source may not forge framework-owned flag alias evidence:\n' >&2
fi
printf ' %s: protected alias evidence token %s\n' "$relative_path" "$token" >&2
source_policy_failed=true
;;
esac
done <<EOF
$matches
EOF
done
if [ "$source_policy_failed" = true ]; then
exit 2
fi
}
install_authority_helper() {
source_root="$1"
worktree="$2"
include_alias_contract="$3"
[ "$source_root" = "$worktree" ] && return
for helper_path in cmd/interface-snapshot internal/interfacesnapshot; do
rm -rf "$worktree/$helper_path"
mkdir -p "$worktree/$helper_path"
cp -R "$source_root/$helper_path/." "$worktree/$helper_path/"
done
if [ "$include_alias_contract" = true ]; then
mkdir -p "$worktree/$(dirname "$ALIAS_CONTRACT_REL")"
rm -f "$worktree/$ALIAS_CONTRACT_REL"
cp "$source_root/$ALIAS_CONTRACT_REL" "$worktree/$ALIAS_CONTRACT_REL"
fi
}
EMPTY_MANIFEST="$TMP_ROOT/empty-migrations.json"
printf '%s\n' '{"version":1,"migrations":[]}' >"$EMPTY_MANIFEST"
USE_MIGRATION_GOVERNANCE=false
if has_complete_migration_governance "$BASE_COMMIT"; then
USE_MIGRATION_GOVERNANCE=true
AUTHORITY_ROOT="$BASE_WORKTREE"
APPROVED_MANIFEST="$BASE_WORKTREE/$MIGRATION_MANIFEST_REL"
CANDIDATE_MANIFEST="$CANDIDATE_WORKTREE/$MIGRATION_MANIFEST_REL"
require_complete_candidate_governance
check_candidate_alias_source_policy
install_authority_helper "$AUTHORITY_ROOT" "$CANDIDATE_WORKTREE" true
else
# One-time bootstrap for the governance PR itself. No base approval exists,
# so the existing base-owned modern helper performs an ordinary comparison
# and this path admits only the canonical empty ledger. The candidate's new
# comparator never participates in the bootstrap decision.
if has_any_migration_governance_artifact "$BASE_COMMIT"; then
printf 'merge-base contains an incomplete flag migration governance artifact set: %s\n' \
"$BASE_REF" >&2
exit 2
fi
if ! has_modern_interface_helper "$BASE_COMMIT"; then
printf 'merge-base lacks the modern interface snapshot helper required for governance bootstrap: %s\n' "$BASE_REF" >&2
exit 2
fi
AUTHORITY_ROOT="$BASE_WORKTREE"
require_complete_candidate_governance
check_candidate_alias_source_policy
BOOTSTRAP_MANIFEST="$CANDIDATE_WORKTREE/$MIGRATION_MANIFEST_REL"
if ! cmp -s "$BOOTSTRAP_MANIFEST" "$EMPTY_MANIFEST"; then
printf 'initial flag migration governance requires the canonical empty manifest: %s\n' "$BOOTSTRAP_MANIFEST" >&2
exit 2
fi
install_authority_helper "$AUTHORITY_ROOT" "$CANDIDATE_WORKTREE" false
fi
install_authority_helper "$AUTHORITY_ROOT" "$BASE_WORKTREE" "$USE_MIGRATION_GOVERNANCE"
install_authority_helper "$AUTHORITY_ROOT" "$STABLE_WORKTREE" "$USE_MIGRATION_GOVERNANCE"
generate_snapshot() {
worktree="$1"
output="$2"
(
cd "$worktree"
go run ./cmd/interface-snapshot generate --output "$output"
@@ -112,11 +276,26 @@ CANDIDATE="$TMP_ROOT/candidate.json"
BASELINE="$TMP_ROOT/merge-base.json"
STABLE="$TMP_ROOT/stable.json"
go run ./cmd/interface-snapshot generate --output "$CANDIDATE"
generate_ref_snapshot "$BASE_REF" "$BASE_WORKTREE" "$BASELINE"
generate_ref_snapshot "$STABLE_REF" "$STABLE_WORKTREE" "$STABLE"
generate_snapshot "$CANDIDATE_WORKTREE" "$CANDIDATE"
generate_snapshot "$BASE_WORKTREE" "$BASELINE"
generate_snapshot "$STABLE_WORKTREE" "$STABLE"
go run ./cmd/interface-snapshot compare \
--current "$CANDIDATE" \
--base "$BASELINE" \
--stable "$STABLE"
if [ "$USE_MIGRATION_GOVERNANCE" = true ]; then
(
cd "$AUTHORITY_ROOT"
go run ./cmd/interface-snapshot compare \
--current "$CANDIDATE" \
--base "$BASELINE" \
--stable "$STABLE" \
--approved-flag-migrations "$APPROVED_MANIFEST" \
--candidate-flag-migrations "$CANDIDATE_MANIFEST"
)
else
(
cd "$AUTHORITY_ROOT"
go run ./cmd/interface-snapshot compare \
--current "$CANDIDATE" \
--base "$BASELINE" \
--stable "$STABLE"
)
fi
@@ -0,0 +1,454 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package commandcompat
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
const authorityHelperSource = `package main
import (
"flag"
"fmt"
"os"
"example.com/dws-command-compat-authority-test/internal/interfacesnapshot"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "missing command")
os.Exit(2)
}
switch os.Args[1] {
case "generate":
flags := flag.NewFlagSet("generate", flag.ExitOnError)
output := flags.String("output", "", "snapshot output")
_ = flags.Parse(os.Args[2:])
if *output == "" {
fmt.Fprintln(os.Stderr, "missing --output")
os.Exit(2)
}
marker := interfacesnapshot.AuthorityMarker()
if err := os.WriteFile(*output, []byte("AUTHORITY="+marker+"\n"), 0o600); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
fmt.Printf("BASE_HELPER_GENERATE=%s\n", marker)
case "compare":
flags := flag.NewFlagSet("compare", flag.ExitOnError)
candidateManifest := flags.String("candidate-flag-migrations", "", "candidate manifest")
current := flags.String("current", "", "current snapshot")
base := flags.String("base", "", "base snapshot")
stable := flags.String("stable", "", "stable snapshot")
_ = flags.String("approved-flag-migrations", "", "approved manifest")
_ = flags.Parse(os.Args[2:])
for _, path := range []string{*current, *base, *stable} {
content, err := os.ReadFile(path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if string(content) != "AUTHORITY=BASE\n" {
fmt.Fprintf(os.Stderr, "snapshot did not use base internal helper: %s", content)
os.Exit(2)
}
}
if *candidateManifest != "" {
content, err := os.ReadFile(*candidateManifest)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
fmt.Printf("CANDIDATE_MANIFEST=%s\n", *candidateManifest)
if string(content) != "{\"version\":1,\"migrations\":[]}\n" {
fmt.Println("UNCOMMITTED_MANIFEST_USED")
return
}
}
fmt.Fprintln(os.Stderr, "BASE_COMPARATOR_ENFORCED")
os.Exit(23)
default:
fmt.Fprintln(os.Stderr, "unknown command")
os.Exit(2)
}
}
`
const governedSnapshotSource = `package interfacesnapshot
import "example.com/dws-command-compat-authority-test/internal/corecmd/runtimeannotate"
var (
_ = runtimeannotate.AnnotationFlagAliasOf
_ = runtimeannotate.AnnotationFlagAliasOrigin
_ = runtimeannotate.FlagAliasOriginCorecmdV1
)
func AuthorityMarker() string { return "BASE" }
`
const baseSnapshotSource = `package interfacesnapshot
func AuthorityMarker() string { return "BASE" }
`
const stableSnapshotSource = `package interfacesnapshot
func AuthorityMarker() string { return "STABLE" }
`
const candidateSnapshotSource = `package interfacesnapshot
func AuthorityMarker() string { return "CANDIDATE" }
`
const aliasContractSource = `package runtimeannotate
const (
AnnotationFlagAliasOf = "dws.compat.alias_of"
AnnotationFlagAliasOrigin = "dws.compat.alias_origin"
FlagAliasOriginCorecmdV1 = "corecmd.flag_spec_aliases.v1"
)
`
const candidateNoopSource = `package main
import "fmt"
func main() {
fmt.Println("CANDIDATE_NOOP")
}
`
type authorityScenario struct {
name string
baseGovernance string
candidateMutation string
want string
wantAuthorityDecision bool
}
func TestCrossPlatformCoverageCommandCompatibilityUsesBaseOwnedAuthority(t *testing.T) {
tests := []authorityScenario{
{
name: "governed base rejects candidate helper and live manifest",
baseGovernance: "complete",
wantAuthorityDecision: true,
},
{
name: "bootstrap uses old base helper",
baseGovernance: "none",
wantAuthorityDecision: true,
},
{
name: "partial base fails closed",
baseGovernance: "partial",
want: "incomplete flag migration governance artifact set",
},
{
name: "base ledger ancestor symlink fails closed",
baseGovernance: "ancestor-symlink",
want: "incomplete flag migration governance artifact set",
},
{
name: "governed candidate cannot delete alias contract",
baseGovernance: "complete",
candidateMutation: "delete-alias-contract",
want: "must preserve the complete flag migration governance artifact set",
},
{
name: "governed candidate cannot replace helper parent with symlink",
baseGovernance: "complete",
candidateMutation: "symlink-helper-parent",
want: "must preserve the complete flag migration governance artifact set",
},
{
name: "candidate cannot forge alias evidence token",
baseGovernance: "complete",
candidateMutation: "forge-alias-token",
want: "may not forge framework-owned flag alias evidence",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
runBaseOwnedAuthorityCase(t, test)
})
}
}
func runBaseOwnedAuthorityCase(t *testing.T, test authorityScenario) {
t.Helper()
repoRoot := repositoryRoot(t)
managedRoot := filepath.Join(repoRoot, ".worktrees", "commandcompat-authority-integration")
if err := os.MkdirAll(managedRoot, 0o755); err != nil {
t.Fatalf("create managed test root: %v", err)
}
fixtureRoot, err := os.MkdirTemp(managedRoot, fmt.Sprintf("case-%d-", time.Now().UnixNano()))
if err != nil {
t.Fatalf("create managed fixture repo: %v", err)
}
t.Cleanup(func() {
if err := os.RemoveAll(fixtureRoot); err != nil {
t.Errorf("remove managed fixture repo: %v", err)
}
})
copyFile(t,
filepath.Join(repoRoot, "scripts", "policy", "check-command-compatibility.sh"),
filepath.Join(fixtureRoot, "scripts", "policy", "check-command-compatibility.sh"),
0o755,
)
copyFile(t,
filepath.Join(repoRoot, "scripts", "policy", "policy-runtime.sh"),
filepath.Join(fixtureRoot, "scripts", "policy", "policy-runtime.sh"),
0o755,
)
copyFile(t,
filepath.Join(repoRoot, "scripts", "release", "release-lib.sh"),
filepath.Join(fixtureRoot, "scripts", "release", "release-lib.sh"),
0o755,
)
writeFile(t, filepath.Join(fixtureRoot, "go.mod"), "module example.com/dws-command-compat-authority-test\n\ngo 1.23\n", 0o644)
writeFile(t, filepath.Join(fixtureRoot, "cmd", "interface-snapshot", "main.go"), authorityHelperSource, 0o644)
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "snapshot.go"), stableSnapshotSource, 0o644)
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "compare.go"), "package interfacesnapshot\n", 0o644)
run(t, fixtureRoot, "git", "init", "-b", "main")
run(t, fixtureRoot, "git", "config", "user.name", "Command Compatibility Test")
run(t, fixtureRoot, "git", "config", "user.email", "command-compat-test@example.invalid")
run(t, fixtureRoot, "git", "add", ".")
run(t, fixtureRoot, "git", "commit", "-m", "old stable without migration governance")
stableCommit := strings.TrimSpace(run(t, fixtureRoot, "git", "rev-parse", "HEAD"))
run(t, fixtureRoot, "git", "tag", "v1.0.0", stableCommit)
writeFile(t, filepath.Join(fixtureRoot, "BASE_MARKER"), "newer merge-base\n", 0o644)
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "snapshot.go"), baseSnapshotSource, 0o644)
switch test.baseGovernance {
case "complete":
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "snapshot.go"), governedSnapshotSource, 0o644)
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "migrations.go"), "package interfacesnapshot\n", 0o644)
writeFile(t,
filepath.Join(fixtureRoot, "internal", "corecmd", "runtimeannotate", "interface_alias.go"),
aliasContractSource,
0o644,
)
writeFile(t,
filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json"),
"{\"version\":1,\"migrations\":[]}\n",
0o644,
)
case "partial":
writeFile(t,
filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json"),
"{\"version\":1,\"migrations\":[]}\n",
0o644,
)
case "ancestor-symlink":
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "migrations.go"), "package interfacesnapshot\n", 0o644)
writeFile(t,
filepath.Join(fixtureRoot, "internal", "corecmd", "runtimeannotate", "interface_alias.go"),
aliasContractSource,
0o644,
)
writeFile(t,
filepath.Join(fixtureRoot, "scripts", "policy", "authority-ledger", "approved-flag-migrations-v1.json"),
"{\"version\":1,\"migrations\":[]}\n",
0o644,
)
if err := os.Symlink("authority-ledger", filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations")); err != nil {
t.Fatalf("symlink base ledger parent: %v", err)
}
case "none":
default:
t.Fatalf("unknown base governance state %q", test.baseGovernance)
}
run(t, fixtureRoot, "git", "add", ".")
run(t, fixtureRoot, "git", "commit", "-m", "base authority")
baseRef := strings.TrimSpace(run(t, fixtureRoot, "git", "rev-parse", "HEAD"))
if baseRef == stableCommit {
t.Fatal("fixture stable tag must predate the governed merge-base")
}
// A withdrawn higher GA tag is reserved history, not the active stable
// compatibility baseline. The checker must agree with release-contract.sh.
run(t, fixtureRoot, "git", "tag", "v1.1.0", baseRef)
run(t, fixtureRoot, "git", "update-ref", "refs/tags/withdrawn/v1.1.0", baseRef)
// 候选提交故意把生成器与比较器改成无条件成功;若脚本错误地信任候选代码,
// 整个检查会输出 CANDIDATE_NOOP 并以 0 退出。
writeFile(t, filepath.Join(fixtureRoot, "cmd", "interface-snapshot", "main.go"), candidateNoopSource, 0o644)
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "snapshot.go"), candidateSnapshotSource, 0o644)
if test.baseGovernance != "complete" {
writeFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "migrations.go"), "package interfacesnapshot\n", 0o644)
writeFile(t,
filepath.Join(fixtureRoot, "internal", "corecmd", "runtimeannotate", "interface_alias.go"),
aliasContractSource,
0o644,
)
writeFile(t,
filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json"),
"{\"version\":1,\"migrations\":[]}\n",
0o644,
)
}
switch test.candidateMutation {
case "":
case "delete-alias-contract":
if err := os.Remove(filepath.Join(fixtureRoot, "internal", "corecmd", "runtimeannotate", "interface_alias.go")); err != nil {
t.Fatalf("remove candidate alias contract: %v", err)
}
case "forge-alias-token":
writeFile(t,
filepath.Join(fixtureRoot, "internal", "evil", "forged.go"),
"package evil\n\nconst forged = \"dws.compat.alias_of\"\n",
0o644,
)
case "symlink-helper-parent":
helperPath := filepath.Join(fixtureRoot, "internal", "interfacesnapshot")
alternatePath := filepath.Join(fixtureRoot, "internal", "interfacesnapshot-alt")
if err := os.Rename(helperPath, alternatePath); err != nil {
t.Fatalf("move candidate helper behind ancestor symlink: %v", err)
}
if err := os.Symlink("interfacesnapshot-alt", helperPath); err != nil {
t.Fatalf("symlink candidate helper parent: %v", err)
}
default:
t.Fatalf("unknown candidate mutation %q", test.candidateMutation)
}
run(t, fixtureRoot, "git", "add", ".")
run(t, fixtureRoot, "git", "commit", "-m", "malicious candidate comparator")
if test.baseGovernance == "complete" && test.candidateMutation == "" {
writeFile(t,
filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json"),
"{\"version\":1,\"migrations\":[{\"uncommitted\":true}]}\n",
0o644,
)
}
cmd := exec.Command(
"sh",
"scripts/policy/check-command-compatibility.sh",
"--base-ref", baseRef,
"--stable-ref", "v1.0.0",
"--candidate-ref", "HEAD",
)
cmd.Dir = fixtureRoot
cmd.Env = integrationEnvironment(fixtureRoot)
output, runErr := cmd.CombinedOutput()
got := string(output)
if runErr == nil {
t.Fatalf("compatibility script unexpectedly succeeded; output:\n%s", got)
}
if strings.Contains(got, "CANDIDATE_NOOP") {
t.Fatalf("candidate-owned no-op helper took control; output:\n%s", got)
}
if strings.Contains(got, "UNCOMMITTED_MANIFEST_USED") {
t.Fatalf("compatibility script mixed the live manifest with the committed candidate surface; output:\n%s", got)
}
if test.wantAuthorityDecision {
if count := strings.Count(got, "BASE_HELPER_GENERATE=BASE"); count != 3 {
t.Fatalf("compatibility script generated %d/3 snapshots with the base-owned internal helper; output:\n%s", count, got)
}
if !strings.Contains(got, "BASE_COMPARATOR_ENFORCED") {
t.Fatalf("compatibility script did not use the base-owned comparator; output:\n%s", got)
}
if test.baseGovernance == "complete" && !strings.Contains(got, "CANDIDATE_MANIFEST=") {
t.Fatalf("base comparator did not receive the committed candidate manifest; output:\n%s", got)
}
} else if !strings.Contains(got, test.want) {
t.Fatalf("compatibility script output missing %q:\n%s", test.want, got)
}
if !test.wantAuthorityDecision {
return
}
wrongStable := exec.Command(
"sh",
"scripts/policy/check-command-compatibility.sh",
"--base-ref", baseRef,
"--stable-ref", "HEAD",
"--candidate-ref", "HEAD",
)
wrongStable.Dir = fixtureRoot
wrongStable.Env = integrationEnvironment(fixtureRoot)
wrongOutput, wrongErr := wrongStable.CombinedOutput()
if wrongErr == nil || !strings.Contains(string(wrongOutput), "is not the expected highest GA tag") {
t.Fatalf("arbitrary stable ref was not rejected: err=%v output:\n%s", wrongErr, wrongOutput)
}
}
func repositoryRoot(t *testing.T) string {
t.Helper()
_, sourceFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("resolve test source path")
}
root := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "..", "..", ".."))
if _, err := os.Stat(filepath.Join(root, "scripts", "policy", "check-command-compatibility.sh")); err != nil {
t.Fatalf("resolve repository root %s: %v", root, err)
}
return root
}
func copyFile(t *testing.T, source, destination string, mode os.FileMode) {
t.Helper()
content, err := os.ReadFile(source)
if err != nil {
t.Fatalf("read %s: %v", source, err)
}
writeFile(t, destination, string(content), mode)
}
func writeFile(t *testing.T, path, content string, mode os.FileMode) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("create parent for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), mode); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
func run(t *testing.T, dir, name string, args ...string) string {
t.Helper()
cmd := exec.Command(name, args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%s %s failed: %v\n%s", name, strings.Join(args, " "), err, output)
}
return string(output)
}
func integrationEnvironment(fixtureRoot string) []string {
overrides := map[string]string{
"DWS_POLICY_TMPDIR": "",
"GOCACHE": filepath.Join(fixtureRoot, ".cache", "go-build"),
"GOMODCACHE": filepath.Join(fixtureRoot, ".cache", "go-mod"),
"GOTMPDIR": "",
"GOTOOLCHAIN": "local",
"GOWORK": "off",
}
environment := make([]string, 0, len(os.Environ())+len(overrides))
for _, item := range os.Environ() {
key, _, _ := strings.Cut(item, "=")
if _, replaced := overrides[key]; !replaced {
environment = append(environment, item)
}
}
for key, value := range overrides {
environment = append(environment, key+"="+value)
}
return environment
}
@@ -0,0 +1 @@
{"version":1,"migrations":[]}
+427
View File
@@ -15,6 +15,8 @@ import (
"reflect"
"sort"
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot"
)
const schemaContractVersion = 3
@@ -83,12 +85,19 @@ func main() {
func run(args []string, stdout, stderr io.Writer) int {
var normalizePath, checkPath, mergePath, currentPath string
var approvedFlagMigrationsPath, candidateFlagMigrationsPath string
var migrationCurrentSnapshotPath, migrationBaseSnapshotPath, migrationStableSnapshotPath string
flags := flag.NewFlagSet("schema-compat", flag.ContinueOnError)
flags.SetOutput(stderr)
flags.StringVar(&normalizePath, "normalize", "", "normalize a raw complete Schema response")
flags.StringVar(&checkPath, "check", "", "check against a normalized historical baseline")
flags.StringVar(&mergePath, "merge", "", "merge additions into a normalized historical baseline")
flags.StringVar(&currentPath, "current", "", "raw current complete Schema response")
flags.StringVar(&approvedFlagMigrationsPath, "approved-flag-migrations", "", "base-owned approved flag migration manifest")
flags.StringVar(&candidateFlagMigrationsPath, "candidate-flag-migrations", "", "detached candidate flag migration manifest")
flags.StringVar(&migrationCurrentSnapshotPath, "migration-current-snapshot", "", "current interface snapshot used for migration authorization")
flags.StringVar(&migrationBaseSnapshotPath, "migration-base-snapshot", "", "merge-base interface snapshot used for migration authorization")
flags.StringVar(&migrationStableSnapshotPath, "migration-stable-snapshot", "", "stable interface snapshot used for migration authorization")
if err := flags.Parse(args); err != nil {
return 2
}
@@ -103,6 +112,27 @@ func run(args []string, stdout, stderr io.Writer) int {
fmt.Fprintln(stderr, "exactly one of --normalize, --check, or --merge is required")
return 2
}
migrationInputs := []string{
approvedFlagMigrationsPath,
candidateFlagMigrationsPath,
migrationCurrentSnapshotPath,
migrationBaseSnapshotPath,
migrationStableSnapshotPath,
}
migrationInputCount := 0
for _, path := range migrationInputs {
if path != "" {
migrationInputCount++
}
}
if migrationInputCount != 0 && migrationInputCount != len(migrationInputs) {
fmt.Fprintln(stderr, "Schema flag migration authorization requires all five migration inputs")
return 2
}
if migrationInputCount != 0 && checkPath == "" {
fmt.Fprintln(stderr, "Schema flag migration authorization is only valid with --check")
return 2
}
if normalizePath != "" {
currentPath = normalizePath
@@ -129,6 +159,24 @@ func run(args []string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "read schema baseline: %v\n", err)
return 2
}
if migrationInputCount != 0 {
migrations, err := authorizeSchemaFlagMigrations(
approvedFlagMigrationsPath,
candidateFlagMigrationsPath,
migrationCurrentSnapshotPath,
migrationBaseSnapshotPath,
migrationStableSnapshotPath,
)
if err != nil {
fmt.Fprintf(stderr, "authorize Schema flag migrations: %v\n", err)
return 2
}
baseline, err = normalizeSchemaFlagMigrations(baseline, current, migrations)
if err != nil {
fmt.Fprintf(stderr, "normalize approved Schema flag migrations: %v\n", err)
return 2
}
}
failures := checkCompatibility(baseline, current)
if len(failures) > 0 {
fmt.Fprintln(stderr, "Schema backwards-compatibility check failed:")
@@ -160,6 +208,57 @@ func run(args []string, stdout, stderr io.Writer) int {
return 0
}
func authorizeSchemaFlagMigrations(
approvedManifestPath string,
candidateManifestPath string,
currentSnapshotPath string,
baseSnapshotPath string,
stableSnapshotPath string,
) ([]interfacesnapshot.FlagMigration, error) {
approved, err := readFlagMigrationManifestFile(approvedManifestPath)
if err != nil {
return nil, fmt.Errorf("read approved flag migrations: %w", err)
}
candidate, err := readFlagMigrationManifestFile(candidateManifestPath)
if err != nil {
return nil, fmt.Errorf("read candidate flag migrations: %w", err)
}
current, err := readInterfaceSnapshotFile(currentSnapshotPath)
if err != nil {
return nil, fmt.Errorf("read migration current snapshot: %w", err)
}
base, err := readInterfaceSnapshotFile(baseSnapshotPath)
if err != nil {
return nil, fmt.Errorf("read migration base snapshot: %w", err)
}
stable, err := readInterfaceSnapshotFile(stableSnapshotPath)
if err != nil {
return nil, fmt.Errorf("read migration stable snapshot: %w", err)
}
return interfacesnapshot.AuthorizeFlagMigrations(
current,
map[string]interfacesnapshot.Snapshot{"merge-base": base, "stable": stable},
approved,
candidate,
)
}
func readFlagMigrationManifestFile(path string) (interfacesnapshot.FlagMigrationManifest, error) {
data, err := os.ReadFile(path)
if err != nil {
return interfacesnapshot.FlagMigrationManifest{}, err
}
return interfacesnapshot.ReadFlagMigrationManifest(bytes.NewReader(data))
}
func readInterfaceSnapshotFile(path string) (interfacesnapshot.Snapshot, error) {
data, err := os.ReadFile(path)
if err != nil {
return interfacesnapshot.Snapshot{}, err
}
return interfacesnapshot.Read(bytes.NewReader(data))
}
func normalizeRawFile(path string) (schemaContract, error) {
data, err := os.ReadFile(path)
if err != nil {
@@ -1052,6 +1151,334 @@ func cloneContract(source schemaContract) schemaContract {
return cloned
}
type schemaToolRef struct {
productID string
toolID string
}
// normalizeSchemaFlagMigrations projects only an already-authorized CLI flag
// rename onto a cloned historical Schema contract. The ordinary compatibility
// checker still makes the final decision; this adapter never drops findings by
// matching their rendered text.
func normalizeSchemaFlagMigrations(
baseline schemaContract,
current schemaContract,
migrations []interfacesnapshot.FlagMigration,
) (schemaContract, error) {
normalized := cloneContract(baseline)
renamesByTool := map[schemaToolRef]map[string]string{}
for _, migration := range migrations {
primaryPath := strings.TrimPrefix(migration.Command, "dws ")
matches := schemaToolsByPrimaryPath(baseline, primaryPath)
if len(matches) == 0 {
// A reviewed CLI-only command has no Schema compatibility surface.
continue
}
if len(matches) != 1 {
return schemaContract{}, fmt.Errorf(
"approved flag migration %q matches %d historical Schema tools",
migration.Command,
len(matches),
)
}
ref := matches[0]
oldTool := baseline.Products[ref.productID].Tools[ref.toolID]
newProduct, productExists := current.Products[ref.productID]
newTool, toolExists := newProduct.Tools[ref.toolID]
if !productExists || !toolExists || newTool.PrimaryCLIPath != primaryPath {
// Preserve the original baseline so the ordinary checker reports the
// missing tool or changed primary_cli_path.
continue
}
oldLegacy, legacyExisted := oldTool.Parameters[migration.Legacy.Name]
oldCanonical, canonicalExisted := oldTool.Parameters[migration.Canonical.Name]
if !legacyExisted && !canonicalExisted {
// A CLI migration is not evidence that an unrelated Schema surface
// published either name. In particular, it must not authorize a
// constraints rewrite with no historical parameter to rename.
continue
}
if _, exists := newTool.Parameters[migration.Legacy.Name]; exists {
return schemaContract{}, fmt.Errorf(
"approved flag migration %q still publishes legacy Schema parameter %q",
migration.Command,
migration.Legacy.Name,
)
}
newCanonical, exists := newTool.Parameters[migration.Canonical.Name]
if !exists {
return schemaContract{}, fmt.Errorf(
"approved flag migration %q does not publish canonical Schema parameter %q",
migration.Command,
migration.Canonical.Name,
)
}
if migration.Canonical.After.Required && (!newCanonical.Required || !newCanonical.CLIRequired) {
return schemaContract{}, fmt.Errorf(
"approved flag migration %q canonical Schema parameter %q must be required and cli_required",
migration.Command,
migration.Canonical.Name,
)
}
if !legacyExisted {
// A consumed receipt may be evaluated after the merge-base Schema has
// already reached the canonical-only state. In that case the ordinary
// checker needs no projection. More importantly, a CLI migration is not
// authority to promote a canonical-only historical Schema parameter from
// optional to required: without legacy Schema evidence there is no rename.
continue
}
normalizedProduct := normalized.Products[ref.productID]
normalizedTool := normalizedProduct.Tools[ref.toolID]
if err := validateRenamedSchemaParameter(migration, oldLegacy, newCanonical); err != nil {
return schemaContract{}, err
}
delete(normalizedTool.Parameters, migration.Legacy.Name)
if canonicalExisted {
if err := validateRenamedSchemaParameter(migration, oldCanonical, newCanonical); err != nil {
return schemaContract{}, err
}
normalizedCanonical := normalizedTool.Parameters[migration.Canonical.Name]
normalizedCanonical.Required = newCanonical.Required
normalizedCanonical.CLIRequired = newCanonical.CLIRequired
normalizedTool.Parameters[migration.Canonical.Name] = normalizedCanonical
} else {
normalizedTool.Parameters[migration.Canonical.Name] = newCanonical
}
normalizedProduct.Tools[ref.toolID] = normalizedTool
normalized.Products[ref.productID] = normalizedProduct
if renamesByTool[ref] == nil {
renamesByTool[ref] = map[string]string{}
}
if existing, ok := renamesByTool[ref][migration.Legacy.Name]; ok && existing != migration.Canonical.Name {
return schemaContract{}, fmt.Errorf(
"approved flag migration %q maps Schema parameter %q to both %q and %q",
migration.Command,
migration.Legacy.Name,
existing,
migration.Canonical.Name,
)
}
renamesByTool[ref][migration.Legacy.Name] = migration.Canonical.Name
}
for ref, renames := range renamesByTool {
oldTool := baseline.Products[ref.productID].Tools[ref.toolID]
newTool := current.Products[ref.productID].Tools[ref.toolID]
if oldTool.Constraints == newTool.Constraints {
continue
}
oldConstraints, oldOK := canonicalizeMigratedConstraints(oldTool.Constraints, renames)
newConstraints, newOK := canonicalizeMigratedConstraints(newTool.Constraints, nil)
if !oldOK || !newOK || oldConstraints != newConstraints {
// Keep the historical constraints unchanged. The ordinary checker
// will report every non-rename constraint change.
continue
}
product := normalized.Products[ref.productID]
tool := product.Tools[ref.toolID]
tool.Constraints = newTool.Constraints
product.Tools[ref.toolID] = tool
normalized.Products[ref.productID] = product
}
return normalized, nil
}
func schemaToolsByPrimaryPath(contract schemaContract, primaryPath string) []schemaToolRef {
var matches []schemaToolRef
for productID, product := range contract.Products {
for toolID, tool := range product.Tools {
if tool.PrimaryCLIPath == primaryPath {
matches = append(matches, schemaToolRef{productID: productID, toolID: toolID})
}
}
}
sort.Slice(matches, func(i, j int) bool {
if matches[i].productID != matches[j].productID {
return matches[i].productID < matches[j].productID
}
return matches[i].toolID < matches[j].toolID
})
return matches
}
func validateRenamedSchemaParameter(
migration interfacesnapshot.FlagMigration,
oldParameter parameterSchema,
newParameter parameterSchema,
) error {
if oldParameter.Type != newParameter.Type ||
oldParameter.Property != newParameter.Property ||
oldParameter.InterfaceType != newParameter.InterfaceType ||
oldParameter.RequiredWhen != newParameter.RequiredWhen ||
oldParameter.Default != newParameter.Default ||
oldParameter.InterfaceDefault != newParameter.InterfaceDefault ||
oldParameter.Format != newParameter.Format ||
!equalStringSlices(oldParameter.Enum, newParameter.Enum) {
return fmt.Errorf(
"approved flag migration %q Schema parameter %q -> %q changed a non-migration field",
migration.Command,
migration.Legacy.Name,
migration.Canonical.Name,
)
}
if oldParameter.Required && !newParameter.Required {
return fmt.Errorf(
"approved flag migration %q Schema parameter %q -> %q became optional",
migration.Command,
migration.Legacy.Name,
migration.Canonical.Name,
)
}
if oldParameter.CLIRequired && !newParameter.CLIRequired {
return fmt.Errorf(
"approved flag migration %q Schema parameter %q -> %q stopped being cli_required",
migration.Command,
migration.Legacy.Name,
migration.Canonical.Name,
)
}
return nil
}
func equalStringSlices(left, right []string) bool {
if len(left) != len(right) {
return false
}
for index := range left {
if left[index] != right[index] {
return false
}
}
return true
}
func canonicalizeMigratedConstraints(raw string, renames map[string]string) (string, bool) {
groups, ok := parseMigrationConstraintsStrict(raw)
if !ok {
return "", false
}
normalized := map[string][][]string{}
for _, kind := range []string{"mutually_exclusive", "require_one_of", "require_together"} {
for _, group := range groups[kind] {
members := make([]string, 0, len(group))
for _, member := range group {
if replacement := renames[member]; replacement != "" {
member = replacement
}
members = append(members, member)
}
sort.Strings(members)
members = compactConstraintStrings(members)
normalized[kind] = append(normalized[kind], members)
}
sort.Slice(normalized[kind], func(i, j int) bool {
return strings.Join(normalized[kind][i], "\x00") < strings.Join(normalized[kind][j], "\x00")
})
normalized[kind] = compactConstraintGroups(normalized[kind])
if len(normalized[kind]) == 0 {
delete(normalized, kind)
}
}
if len(normalized) == 0 {
return "", true
}
encoded, err := json.Marshal(normalized)
return string(encoded), err == nil
}
func parseMigrationConstraintsStrict(raw string) (map[string][][]string, bool) {
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return map[string][][]string{}, true
}
decoder := json.NewDecoder(strings.NewReader(trimmed))
opening, err := decoder.Token()
if err != nil || opening != json.Delim('{') {
return nil, false
}
groups := map[string][][]string{}
for decoder.More() {
fieldToken, err := decoder.Token()
if err != nil {
return nil, false
}
// encoding/json guarantees that an object member name is a string token.
field := fieldToken.(string)
switch field {
case "mutually_exclusive", "require_one_of", "require_together":
default:
return nil, false
}
if _, duplicate := groups[field]; duplicate {
return nil, false
}
var encodedGroups json.RawMessage
if err := decoder.Decode(&encodedGroups); err != nil {
return nil, false
}
encodedGroups = bytes.TrimSpace(encodedGroups)
if len(encodedGroups) == 0 || encodedGroups[0] != '[' {
return nil, false
}
var fieldGroups [][]string
if err := json.Unmarshal(encodedGroups, &fieldGroups); err != nil {
return nil, false
}
for _, group := range fieldGroups {
if len(group) == 0 {
return nil, false
}
for _, member := range group {
if member == "" || member != strings.TrimSpace(member) {
return nil, false
}
}
}
groups[field] = fieldGroups
}
closing, err := decoder.Token()
if err != nil || closing != json.Delim('}') {
return nil, false
}
if err := decoder.Decode(&struct{}{}); err != io.EOF {
return nil, false
}
return groups, true
}
func compactConstraintStrings(values []string) []string {
if len(values) < 2 {
return values
}
out := values[:1]
for _, value := range values[1:] {
if value != out[len(out)-1] {
out = append(out, value)
}
}
return out
}
func compactConstraintGroups(groups [][]string) [][]string {
if len(groups) < 2 {
return groups
}
out := groups[:1]
for _, group := range groups[1:] {
if !equalStringSlices(group, out[len(out)-1]) {
out = append(out, group)
}
}
return out
}
func writeContract(w io.Writer, contract schemaContract) error {
contract.Version = schemaContractVersion
if contract.Products == nil {
+805
View File
@@ -9,8 +9,12 @@ import (
"errors"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot"
)
const completeSchemaJSON = `{
@@ -1195,3 +1199,804 @@ func TestCrossPlatformCoverageSchemaCompatInterfaceRefRedirect(t *testing.T) {
}
})
}
func TestCrossPlatformCoverageSchemaFlagMigrationNormalizesExactRename(t *testing.T) {
baseline := schemaFlagMigrationContract(false)
current := schemaFlagMigrationContract(true)
normalized, err := normalizeSchemaFlagMigrations(baseline, current, schemaFlagMigrationAuthorizations())
if err != nil {
t.Fatal(err)
}
if failures := checkCompatibility(normalized, current); len(failures) != 0 {
t.Fatalf("exact flag migration should pass after normalization: %v", failures)
}
tool := normalized.Products["chat"].Tools["chat.edit_message"]
for _, legacy := range []string{"group", "id", "msg-id"} {
if _, exists := tool.Parameters[legacy]; exists {
t.Fatalf("normalized baseline retained legacy parameter %q", legacy)
}
}
if canonical := tool.Parameters["conversation-id"]; !canonical.Required || !canonical.CLIRequired {
t.Fatalf("canonical required transition was not normalized: %#v", canonical)
}
if tool.Constraints != current.Products["chat"].Tools["chat.edit_message"].Constraints {
t.Fatalf("constraints were not normalized: %s", tool.Constraints)
}
}
func TestCrossPlatformCoverageSchemaFlagMigrationRejectsSemanticDrift(t *testing.T) {
tests := []struct {
name string
want string
mutate func(*parameterSchema)
}{
{name: "required decline", want: "must be required and cli_required", mutate: func(parameter *parameterSchema) {
parameter.Required = false
}},
{name: "cli required decline", want: "must be required and cli_required", mutate: func(parameter *parameterSchema) {
parameter.CLIRequired = false
}},
{name: "type", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Type = `"integer"` }},
{name: "property", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Property = "other" }},
{name: "interface type", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.InterfaceType = "integer" }},
{name: "required when", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.RequiredWhen = "mode=other" }},
{name: "default", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Default = `"other"` }},
{name: "interface default", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.InterfaceDefault = `"other"` }},
{name: "format", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Format = "uri" }},
{name: "enum", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Enum = []string{"a"} }},
{name: "enum same length", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Enum = []string{"a", "c"} }},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
current := schemaFlagMigrationContract(true)
product := current.Products["chat"]
tool := product.Tools["chat.edit_message"]
canonical := tool.Parameters["message-id"]
test.mutate(&canonical)
tool.Parameters["message-id"] = canonical
product.Tools["chat.edit_message"] = tool
current.Products["chat"] = product
_, err := normalizeSchemaFlagMigrations(
schemaFlagMigrationContract(false),
current,
schemaFlagMigrationAuthorizations(),
)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("normalizeSchemaFlagMigrations() error = %v, want %q", err, test.want)
}
})
}
}
func TestCrossPlatformCoverageSchemaFlagMigrationAdapterBranches(t *testing.T) {
baseline := schemaFlagMigrationContract(false)
current := schemaFlagMigrationContract(true)
noConstraintsBaseline := cloneContract(baseline)
product := noConstraintsBaseline.Products["chat"]
tool := product.Tools["chat.edit_message"]
tool.Constraints = ""
product.Tools["chat.edit_message"] = tool
noConstraintsBaseline.Products["chat"] = product
noConstraintsCurrent := cloneContract(current)
product = noConstraintsCurrent.Products["chat"]
tool = product.Tools["chat.edit_message"]
tool.Constraints = ""
product.Tools["chat.edit_message"] = tool
noConstraintsCurrent.Products["chat"] = product
if _, err := normalizeSchemaFlagMigrations(noConstraintsBaseline, noConstraintsCurrent, schemaFlagMigrationAuthorizations()); err != nil {
t.Fatalf("unchanged empty constraints: %v", err)
}
cliOnly := schemaFlagMigrationAuthorizations()[:1]
cliOnly[0].Command = "dws chat cli-only"
normalized, err := normalizeSchemaFlagMigrations(baseline, current, cliOnly)
if err != nil || !reflect.DeepEqual(normalized, baseline) {
t.Fatalf("CLI-only migration = %#v, %v", normalized, err)
}
duplicate := cloneContract(baseline)
product = duplicate.Products["chat"]
product.Tools["chat.another"] = product.Tools["chat.edit_message"]
duplicate.Products["chat"] = product
duplicate.Products["chat2"] = productSchema{Tools: map[string]toolSchema{
"chat.third": product.Tools["chat.edit_message"],
}}
if matches := schemaToolsByPrimaryPath(duplicate, "chat message edit"); len(matches) != 3 || matches[0].productID != "chat" || matches[0].toolID != "chat.another" {
t.Fatalf("sorted duplicate Schema matches = %#v", matches)
}
if _, err := normalizeSchemaFlagMigrations(duplicate, current, schemaFlagMigrationAuthorizations()[:1]); err == nil || !strings.Contains(err.Error(), "matches 3") {
t.Fatalf("duplicate Schema path error = %v", err)
}
missingCurrent := cloneContract(current)
delete(missingCurrent.Products, "chat")
normalized, err = normalizeSchemaFlagMigrations(baseline, missingCurrent, schemaFlagMigrationAuthorizations())
if err != nil {
t.Fatal(err)
}
if failures := checkCompatibility(normalized, missingCurrent); len(failures) == 0 {
t.Fatal("missing candidate product was normalized away")
}
canonicalOnly := schemaFlagMigrationAuthorizations()[0]
canonicalOnly.Legacy.Name = "legacy-not-published-in-schema"
driftedCanonical := cloneContract(current)
product = driftedCanonical.Products["chat"]
tool = product.Tools["chat.edit_message"]
canonical := tool.Parameters["conversation-id"]
canonical.Property = "different"
tool.Parameters["conversation-id"] = canonical
product.Tools["chat.edit_message"] = tool
driftedCanonical.Products["chat"] = product
normalized, err = normalizeSchemaFlagMigrations(baseline, driftedCanonical, []interfacesnapshot.FlagMigration{canonicalOnly})
if err != nil {
t.Fatal(err)
}
if failures := strings.Join(checkCompatibility(normalized, driftedCanonical), "\n"); !strings.Contains(failures, "changed property") {
t.Fatalf("canonical-only Schema drift was hidden: %s", failures)
}
canonicalOptional := schemaFlagMigrationContract(true)
product = canonicalOptional.Products["chat"]
tool = product.Tools["chat.edit_message"]
canonical = tool.Parameters["conversation-id"]
canonical.Required = false
canonical.CLIRequired = false
tool.Parameters["conversation-id"] = canonical
product.Tools["chat.edit_message"] = tool
canonicalOptional.Products["chat"] = product
normalized, err = normalizeSchemaFlagMigrations(
canonicalOptional,
schemaFlagMigrationContract(true),
[]interfacesnapshot.FlagMigration{canonicalOnly},
)
if err != nil {
t.Fatal(err)
}
if failures := strings.Join(checkCompatibility(normalized, schemaFlagMigrationContract(true)), "\n"); !strings.Contains(failures, "newly required") || !strings.Contains(failures, "newly cli_required") {
t.Fatalf("canonical-only required promotion was hidden: %s", failures)
}
canonicalConflictBaseline := schemaFlagMigrationContract(false)
product = canonicalConflictBaseline.Products["chat"]
tool = product.Tools["chat.edit_message"]
canonical = tool.Parameters["conversation-id"]
canonical.Property = "historicalCanonicalProperty"
tool.Parameters["conversation-id"] = canonical
product.Tools["chat.edit_message"] = tool
canonicalConflictBaseline.Products["chat"] = product
if _, err := normalizeSchemaFlagMigrations(
canonicalConflictBaseline,
schemaFlagMigrationContract(true),
schemaFlagMigrationAuthorizations()[:1],
); err == nil || !strings.Contains(err.Error(), "non-migration field") {
t.Fatalf("historical canonical drift error = %v", err)
}
conflicting := schemaFlagMigrationAuthorizations()[0]
conflicting.Canonical.Name = "other-conversation-id"
conflicting.Legacy.After.AliasOf = conflicting.Canonical.Name
product = current.Products["chat"]
tool = product.Tools["chat.edit_message"]
tool.Parameters[conflicting.Canonical.Name] = tool.Parameters["conversation-id"]
product.Tools["chat.edit_message"] = tool
current.Products["chat"] = product
if _, err := normalizeSchemaFlagMigrations(baseline, current, []interfacesnapshot.FlagMigration{
schemaFlagMigrationAuthorizations()[0],
conflicting,
}); err == nil || !strings.Contains(err.Error(), "maps Schema parameter") {
t.Fatalf("conflicting Schema mapping error = %v", err)
}
old := parameterSchema{Required: true, CLIRequired: true}
if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], old, parameterSchema{CLIRequired: true}); err == nil || !strings.Contains(err.Error(), "became optional") {
t.Fatalf("direct required decline error = %v", err)
}
if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], old, parameterSchema{Required: true}); err == nil || !strings.Contains(err.Error(), "stopped being cli_required") {
t.Fatalf("direct cli_required decline error = %v", err)
}
}
func TestCrossPlatformCoverageSchemaFlagMigrationRejectsPartialAndUnrelatedChanges(t *testing.T) {
baseline := schemaFlagMigrationContract(false)
migrations := schemaFlagMigrationAuthorizations()
current := schemaFlagMigrationContract(true)
product := current.Products["chat"]
tool := product.Tools["chat.edit_message"]
delete(tool.Parameters, "message-id")
product.Tools["chat.edit_message"] = tool
current.Products["chat"] = product
if _, err := normalizeSchemaFlagMigrations(baseline, current, migrations); err == nil || !strings.Contains(err.Error(), "does not publish canonical") {
t.Fatalf("missing canonical error = %v", err)
}
current = schemaFlagMigrationContract(true)
product = current.Products["chat"]
tool = product.Tools["chat.edit_message"]
tool.Parameters["msg-id"] = schemaFlagMigrationContract(false).Products["chat"].Tools["chat.edit_message"].Parameters["msg-id"]
product.Tools["chat.edit_message"] = tool
current.Products["chat"] = product
if _, err := normalizeSchemaFlagMigrations(baseline, current, migrations); err == nil || !strings.Contains(err.Error(), "still publishes legacy") {
t.Fatalf("published legacy error = %v", err)
}
current = schemaFlagMigrationContract(true)
product = current.Products["chat"]
tool = product.Tools["chat.edit_message"]
delete(tool.Parameters, "unrelated")
product.Tools["chat.edit_message"] = tool
current.Products["chat"] = product
normalized, err := normalizeSchemaFlagMigrations(baseline, current, migrations)
if err != nil {
t.Fatal(err)
}
if failures := strings.Join(checkCompatibility(normalized, current), "\n"); !strings.Contains(failures, `lost parameter "unrelated"`) {
t.Fatalf("unrelated parameter removal was hidden: %s", failures)
}
current = schemaFlagMigrationContract(true)
product = current.Products["chat"]
tool = product.Tools["chat.edit_message"]
tool.Constraints = `{"require_one_of":[["conversation-id"]],"require_together":[["conversation-id","unrelated"]]}`
product.Tools["chat.edit_message"] = tool
current.Products["chat"] = product
normalized, err = normalizeSchemaFlagMigrations(baseline, current, migrations)
if err != nil {
t.Fatal(err)
}
if failures := strings.Join(checkCompatibility(normalized, current), "\n"); !strings.Contains(failures, "changed constraints") {
t.Fatalf("unrelated constraint change was hidden: %s", failures)
}
// A CLI ledger entry alone cannot authorize a Schema constraint rewrite
// when the historical Schema published neither the legacy nor canonical
// parameter involved in that entry.
noSchemaEvidence := schemaFlagMigrationContract(false)
product = noSchemaEvidence.Products["chat"]
tool = product.Tools["chat.edit_message"]
for _, name := range []string{"conversation-id", "group", "id", "msg-id"} {
delete(tool.Parameters, name)
}
product.Tools["chat.edit_message"] = tool
noSchemaEvidence.Products["chat"] = product
normalized, err = normalizeSchemaFlagMigrations(noSchemaEvidence, schemaFlagMigrationContract(true), migrations)
if err != nil {
t.Fatal(err)
}
if failures := strings.Join(checkCompatibility(normalized, schemaFlagMigrationContract(true)), "\n"); !strings.Contains(failures, "changed constraints") {
t.Fatalf("constraint rewrite without Schema parameter evidence was hidden: %s", failures)
}
// A baseline that already contains only the canonical parameter may receive
// a required promotion, but that is not evidence that a stray legacy name in
// constraints belongs to the migration.
canonicalOnly := schemaFlagMigrationContract(true)
product = canonicalOnly.Products["chat"]
tool = product.Tools["chat.edit_message"]
tool.Constraints = `{"require_one_of":[["group"]]}`
product.Tools["chat.edit_message"] = tool
canonicalOnly.Products["chat"] = product
normalized, err = normalizeSchemaFlagMigrations(canonicalOnly, schemaFlagMigrationContract(true), migrations)
if err != nil {
t.Fatal(err)
}
if failures := strings.Join(checkCompatibility(normalized, schemaFlagMigrationContract(true)), "\n"); !strings.Contains(failures, "changed constraints") {
t.Fatalf("canonical-only baseline authorized a legacy constraint rewrite: %s", failures)
}
}
func TestCrossPlatformCoverageSchemaFlagMigrationConstraintsFailClosed(t *testing.T) {
for _, constraints := range []string{
`{`,
`{"`,
`{"require_one_of":`,
`{"unknown_kind":[["conversation-id"]]}`,
`{"require_one_of":"conversation-id"}`,
`{"require_one_of":["conversation-id"]}`,
`{"require_one_of":null}`,
`{"require_one_of":[[]]}`,
`{"require_one_of":[[" conversation-id"]]}`,
`{"require_one_of":[["conversation-id"]],"require_one_of":[["conversation-id"]]}`,
`{"require_one_of":[]`,
`{} {}`,
`[]`,
} {
current := schemaFlagMigrationContract(true)
product := current.Products["chat"]
tool := product.Tools["chat.edit_message"]
tool.Constraints = constraints
product.Tools["chat.edit_message"] = tool
current.Products["chat"] = product
normalized, err := normalizeSchemaFlagMigrations(
schemaFlagMigrationContract(false),
current,
schemaFlagMigrationAuthorizations(),
)
if err != nil {
t.Fatal(err)
}
if failures := strings.Join(checkCompatibility(normalized, current), "\n"); !strings.Contains(failures, "changed constraints") {
t.Fatalf("malformed constraints %s were authorized: %s", constraints, failures)
}
}
if encoded, ok := canonicalizeMigratedConstraints("", nil); !ok || encoded != "" {
t.Fatalf("empty migrated constraints = %q, %v", encoded, ok)
}
encoded, ok := canonicalizeMigratedConstraints(
`{"require_one_of":[["b"],["a"],["a"],["c","c"]]}`,
nil,
)
if !ok || encoded != `{"require_one_of":[["a"],["b"],["c"]]}` {
t.Fatalf("canonical migrated constraint groups = %q, %v", encoded, ok)
}
}
func TestCrossPlatformCoverageSchemaMigrationCLIRequiresCompleteCheckInputs(t *testing.T) {
allInputs := []string{
"--approved-flag-migrations", "approved.json",
"--candidate-flag-migrations", "candidate.json",
"--migration-current-snapshot", "current.json",
"--migration-base-snapshot", "base.json",
"--migration-stable-snapshot", "stable.json",
}
var stdout, stderr bytes.Buffer
if code := run([]string{"--check", "baseline.json", "--current", "schema.json", "--approved-flag-migrations", "approved.json"}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "all five") {
t.Fatalf("partial migration inputs code=%d stderr=%q", code, stderr.String())
}
for _, mode := range []string{"--normalize", "--merge"} {
stderr.Reset()
args := append([]string{mode, "schema.json"}, allInputs...)
if code := run(args, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "only valid with --check") {
t.Fatalf("%s migration inputs code=%d stderr=%q", mode, code, stderr.String())
}
}
}
func TestCrossPlatformCoverageSchemaMigrationLifecycleAndRun(t *testing.T) {
directory := t.TempDir()
baselinePath := filepath.Join(directory, "schema-baseline.json")
currentSchemaPath := filepath.Join(directory, "schema-current.json")
writeSchemaContractFile(t, baselinePath, schemaFlagMigrationContract(false))
writeRawSchemaContractFile(t, currentSchemaPath, schemaFlagMigrationContract(true))
approvedPath := filepath.Join(directory, "approved.json")
candidatePath := filepath.Join(directory, "candidate.json")
currentSnapshotPath := filepath.Join(directory, "interface-current.json")
baseSnapshotPath := filepath.Join(directory, "interface-base.json")
stableSnapshotPath := filepath.Join(directory, "interface-stable.json")
writeFlagMigrationManifestFile(t, approvedPath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationPending))
writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed))
writeInterfaceSnapshotFile(t, currentSnapshotPath, schemaFlagMigrationSnapshot(true))
writeInterfaceSnapshotFile(t, baseSnapshotPath, schemaFlagMigrationSnapshot(false))
writeInterfaceSnapshotFile(t, stableSnapshotPath, schemaFlagMigrationSnapshot(false))
args := []string{
"--check", baselinePath,
"--current", currentSchemaPath,
"--approved-flag-migrations", approvedPath,
"--candidate-flag-migrations", candidatePath,
"--migration-current-snapshot", currentSnapshotPath,
"--migration-base-snapshot", baseSnapshotPath,
"--migration-stable-snapshot", stableSnapshotPath,
}
var stdout, stderr bytes.Buffer
if code := run(args, &stdout, &stderr); code != 0 {
t.Fatalf("governed Schema check code=%d stderr=%s", code, stderr.String())
}
if !strings.Contains(stdout.String(), "compatibility check: ok") {
t.Fatalf("governed Schema check output = %q", stdout.String())
}
badArgs := append([]string(nil), args...)
badArgs[5] = filepath.Join(directory, "missing-approved.json")
stderr.Reset()
if code := run(badArgs, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "authorize Schema flag migrations") {
t.Fatalf("authorization error code=%d stderr=%q", code, stderr.String())
}
legacyCurrent := schemaFlagMigrationContract(true)
product := legacyCurrent.Products["chat"]
tool := product.Tools["chat.edit_message"]
tool.Parameters["msg-id"] = schemaFlagMigrationContract(false).Products["chat"].Tools["chat.edit_message"].Parameters["msg-id"]
product.Tools["chat.edit_message"] = tool
legacyCurrent.Products["chat"] = product
writeRawSchemaContractFile(t, currentSchemaPath, legacyCurrent)
stderr.Reset()
if code := run(args, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "normalize approved Schema flag migrations") {
t.Fatalf("normalization error code=%d stderr=%q", code, stderr.String())
}
// Candidate-owned pending records cannot approve their own after-state.
writeFlagMigrationManifestFile(t, approvedPath, interfacesnapshot.FlagMigrationManifest{
Version: interfacesnapshot.FlagMigrationManifestVersion,
Migrations: []interfacesnapshot.FlagMigration{},
})
writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationPending))
if _, err := authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath); err == nil || !strings.Contains(err.Error(), "cannot authorize its own interface change") {
t.Fatalf("self-approval error = %v", err)
}
// An incomplete after-state fails lifecycle authorization before Schema is normalized.
writeFlagMigrationManifestFile(t, approvedPath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationPending))
writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed))
partial := schemaFlagMigrationSnapshot(true)
partial.Commands[0].LocalFlags = partial.Commands[0].LocalFlags[:len(partial.Commands[0].LocalFlags)-1]
writeInterfaceSnapshotFile(t, currentSnapshotPath, partial)
if _, err := authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath); err == nil || !strings.Contains(err.Error(), "partially applied") {
t.Fatalf("partial migration error = %v", err)
}
}
func TestCrossPlatformCoverageSchemaMigrationFileErrors(t *testing.T) {
directory := t.TempDir()
approvedPath := filepath.Join(directory, "approved.json")
candidatePath := filepath.Join(directory, "candidate.json")
currentPath := filepath.Join(directory, "current.json")
basePath := filepath.Join(directory, "base.json")
stablePath := filepath.Join(directory, "stable.json")
writeFlagMigrationManifestFile(t, approvedPath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationPending))
writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed))
writeInterfaceSnapshotFile(t, currentPath, schemaFlagMigrationSnapshot(true))
writeInterfaceSnapshotFile(t, basePath, schemaFlagMigrationSnapshot(false))
writeInterfaceSnapshotFile(t, stablePath, schemaFlagMigrationSnapshot(false))
paths := []string{approvedPath, candidatePath, currentPath, basePath, stablePath}
wants := []string{
"read approved flag migrations",
"read candidate flag migrations",
"read migration current snapshot",
"read migration base snapshot",
"read migration stable snapshot",
}
for index, want := range wants {
t.Run(want, func(t *testing.T) {
invalid := append([]string(nil), paths...)
invalid[index] = filepath.Join(directory, "missing.json")
_, err := authorizeSchemaFlagMigrations(invalid[0], invalid[1], invalid[2], invalid[3], invalid[4])
if err == nil || !strings.Contains(err.Error(), want) {
t.Fatalf("authorizeSchemaFlagMigrations() error = %v, want %q", err, want)
}
})
}
}
func TestCrossPlatformCoverageSchemaConsumedReceiptIsNoOpForAfterBaseline(t *testing.T) {
directory := t.TempDir()
approvedPath := filepath.Join(directory, "approved.json")
candidatePath := filepath.Join(directory, "candidate.json")
currentSnapshotPath := filepath.Join(directory, "current.json")
baseSnapshotPath := filepath.Join(directory, "base.json")
stableSnapshotPath := filepath.Join(directory, "stable.json")
writeFlagMigrationManifestFile(t, approvedPath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed))
writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed))
writeInterfaceSnapshotFile(t, currentSnapshotPath, schemaFlagMigrationSnapshot(true))
writeInterfaceSnapshotFile(t, baseSnapshotPath, schemaFlagMigrationSnapshot(true))
writeInterfaceSnapshotFile(t, stableSnapshotPath, schemaFlagMigrationSnapshot(false))
migrations, err := authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath)
if err != nil {
t.Fatal(err)
}
baseline := schemaFlagMigrationContract(true)
normalized, err := normalizeSchemaFlagMigrations(baseline, schemaFlagMigrationContract(true), migrations)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(normalized, baseline) {
t.Fatalf("consumed receipt changed an already-after Schema baseline: %#v", normalized)
}
if failures := checkCompatibility(normalized, schemaFlagMigrationContract(true)); len(failures) != 0 {
t.Fatalf("consumed receipt did not preserve compatibility: %v", failures)
}
// Once stable also reaches after, retaining the consumed receipt is stale;
// deleting it yields no authorization and the already-after Schema passes.
writeInterfaceSnapshotFile(t, stableSnapshotPath, schemaFlagMigrationSnapshot(true))
if _, err := authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath); err == nil || !strings.Contains(err.Error(), "stale after all references reached the after state") {
t.Fatalf("stale consumed receipt error = %v", err)
}
emptyManifest := interfacesnapshot.FlagMigrationManifest{
Version: interfacesnapshot.FlagMigrationManifestVersion,
Migrations: []interfacesnapshot.FlagMigration{},
}
writeFlagMigrationManifestFile(t, candidatePath, emptyManifest)
migrations, err = authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath)
if err != nil || len(migrations) != 0 {
t.Fatalf("cleaned consumed receipt authorizations = %#v, %v", migrations, err)
}
baselinePath := filepath.Join(directory, "schema-baseline.json")
currentSchemaPath := filepath.Join(directory, "schema-current.json")
writeSchemaContractFile(t, baselinePath, baseline)
writeRawSchemaContractFile(t, currentSchemaPath, schemaFlagMigrationContract(true))
args := []string{
"--check", baselinePath,
"--current", currentSchemaPath,
"--approved-flag-migrations", approvedPath,
"--candidate-flag-migrations", candidatePath,
"--migration-current-snapshot", currentSnapshotPath,
"--migration-base-snapshot", baseSnapshotPath,
"--migration-stable-snapshot", stableSnapshotPath,
}
var stdout, stderr bytes.Buffer
if code := run(args, &stdout, &stderr); code != 0 {
t.Fatalf("cleaned consumed receipt check code=%d stderr=%q", code, stderr.String())
}
}
func schemaFlagMigrationContract(after bool) schemaContract {
legacyMessage := parameterSchema{
Type: `"string"`,
Property: "openMessageId",
InterfaceType: "string",
Required: true,
CLIRequired: true,
Default: `"default-message"`,
InterfaceDefault: `"default-message"`,
Format: "id",
Enum: []string{"a", "b"},
}
conversation := parameterSchema{
Type: `"string"`,
Property: "openConversationId",
InterfaceType: "string",
}
parameters := map[string]parameterSchema{
"conversation-id": conversation,
"unrelated": {Type: `"string"`, Property: "unrelated"},
}
constraints := `{"require_one_of":[["conversation-id","group","id"]]}`
if after {
conversation.Required = true
conversation.CLIRequired = true
parameters["conversation-id"] = conversation
parameters["message-id"] = legacyMessage
constraints = `{"require_one_of":[["conversation-id"]]}`
} else {
parameters["group"] = conversation
parameters["id"] = conversation
parameters["msg-id"] = legacyMessage
}
return schemaContract{Version: schemaContractVersion, Products: map[string]productSchema{
"chat": {Tools: map[string]toolSchema{
"chat.edit_message": {
PrimaryCLIPath: "chat message edit",
InterfaceMode: "mcp",
Availability: "available",
Parameters: parameters,
Constraints: constraints,
Effect: "write",
Risk: "medium",
Confirmation: "not_required",
Idempotency: "unknown",
},
}},
}}
}
func schemaFlagMigrationAuthorizations() []interfacesnapshot.FlagMigration {
conversationBefore := interfacesnapshot.FlagMigrationState{
Present: true,
Type: "string",
Scope: "local",
}
conversationAfter := conversationBefore
conversationAfter.Required = true
messageBefore := interfacesnapshot.FlagMigrationState{
Present: true,
Type: "string",
Required: true,
Scope: "local",
}
messageAfter := messageBefore
return []interfacesnapshot.FlagMigration{
{
Command: "dws chat message edit",
Legacy: interfacesnapshot.FlagMigrationSide{
Name: "group",
Before: conversationBefore,
After: interfacesnapshot.FlagMigrationState{
Present: true, Type: "string", Hidden: true, Scope: "local", AliasOf: "conversation-id",
},
},
Canonical: interfacesnapshot.FlagMigrationSide{
Name: "conversation-id",
Before: conversationBefore,
After: conversationAfter,
},
},
{
Command: "dws chat message edit",
Legacy: interfacesnapshot.FlagMigrationSide{
Name: "id",
Before: conversationBefore,
After: interfacesnapshot.FlagMigrationState{
Present: true, Type: "string", Hidden: true, Scope: "local", AliasOf: "conversation-id",
},
},
Canonical: interfacesnapshot.FlagMigrationSide{
Name: "conversation-id",
Before: conversationBefore,
After: conversationAfter,
},
},
{
Command: "dws chat message edit",
Legacy: interfacesnapshot.FlagMigrationSide{
Name: "msg-id",
Before: messageBefore,
After: interfacesnapshot.FlagMigrationState{
Present: true, Type: "string", Hidden: true, Scope: "local", AliasOf: "message-id",
},
},
Canonical: interfacesnapshot.FlagMigrationSide{
Name: "message-id",
Before: interfacesnapshot.FlagMigrationState{},
After: messageAfter,
},
},
}
}
func schemaFlagMigrationManifest(state string) interfacesnapshot.FlagMigrationManifest {
migrations := schemaFlagMigrationAuthorizations()
for index := range migrations {
migrations[index].State = state
migrations[index].Reason = "canonicalize historical public flag names"
}
return interfacesnapshot.FlagMigrationManifest{
Version: interfacesnapshot.FlagMigrationManifestVersion,
Migrations: migrations,
}
}
func schemaFlagMigrationSnapshot(after bool) interfacesnapshot.Snapshot {
states := map[string]interfacesnapshot.FlagMigrationState{}
for _, migration := range schemaFlagMigrationAuthorizations() {
if after {
states[migration.Legacy.Name] = migration.Legacy.After
states[migration.Canonical.Name] = migration.Canonical.After
} else {
states[migration.Legacy.Name] = migration.Legacy.Before
states[migration.Canonical.Name] = migration.Canonical.Before
}
}
names := make([]string, 0, len(states))
for name, state := range states {
if state.Present {
names = append(names, name)
}
}
sort.Strings(names)
flags := make([]interfacesnapshot.Flag, 0, len(names))
for _, name := range names {
state := states[name]
flags = append(flags, interfacesnapshot.Flag{
Name: name,
Type: state.Type,
Required: state.Required,
Hidden: state.Hidden,
Shorthand: state.Shorthand,
NoOpt: state.NoOpt,
AliasOf: state.AliasOf,
})
}
return interfacesnapshot.Snapshot{
SchemaVersion: interfacesnapshot.SchemaVersion,
Rules: interfacesnapshot.Rules{
ExcludedCommandSubtrees: []string{},
ExcludedFlags: []string{},
},
Commands: []interfacesnapshot.Command{{
Path: "dws chat message edit",
Runnable: true,
Aliases: []string{},
LocalFlags: flags,
}},
}
}
func writeFlagMigrationManifestFile(t *testing.T, path string, manifest interfacesnapshot.FlagMigrationManifest) {
t.Helper()
data, err := json.MarshalIndent(manifest, "", " ")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, append(data, '\n'), 0o600); err != nil {
t.Fatal(err)
}
}
func writeInterfaceSnapshotFile(t *testing.T, path string, snapshot interfacesnapshot.Snapshot) {
t.Helper()
var encoded bytes.Buffer
if err := interfacesnapshot.Write(&encoded, snapshot); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, encoded.Bytes(), 0o600); err != nil {
t.Fatal(err)
}
}
func writeSchemaContractFile(t *testing.T, path string, contract schemaContract) {
t.Helper()
var encoded bytes.Buffer
if err := writeContract(&encoded, contract); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, encoded.Bytes(), 0o600); err != nil {
t.Fatal(err)
}
}
func writeRawSchemaContractFile(t *testing.T, path string, contract schemaContract) {
t.Helper()
products := make([]map[string]any, 0, len(contract.Products))
for productID, product := range contract.Products {
tools := make([]map[string]any, 0, len(product.Tools))
for toolID, tool := range product.Tools {
parameters := map[string]any{}
for name, parameter := range tool.Parameters {
raw := map[string]any{
"type": json.RawMessage(parameter.Type),
"required": parameter.Required,
"cli_required": parameter.CLIRequired,
"property": parameter.Property,
"interface_type": parameter.InterfaceType,
"required_when": parameter.RequiredWhen,
"format": parameter.Format,
"enum": parameter.Enum,
"field_provenance": map[string]any{},
}
if parameter.Default != "" {
raw["default"] = json.RawMessage(parameter.Default)
}
if parameter.InterfaceDefault != "" {
raw["interface_default"] = json.RawMessage(parameter.InterfaceDefault)
}
parameters[name] = raw
}
rawTool := map[string]any{
"canonical_path": toolID,
"primary_cli_path": tool.PrimaryCLIPath,
"interface_mode": tool.InterfaceMode,
"availability": tool.Availability,
"parameters": parameters,
"effect": tool.Effect,
"risk": tool.Risk,
"confirmation": tool.Confirmation,
"idempotency": tool.Idempotency,
"field_provenance": map[string]any{},
}
if tool.InterfaceRef != "" {
rawTool["interface_ref"] = json.RawMessage(tool.InterfaceRef)
}
if tool.Constraints != "" {
rawTool["constraints"] = json.RawMessage(tool.Constraints)
}
if len(tool.Positionals) > 0 {
rawTool["positionals"] = tool.Positionals
}
if tool.DryRun != "" {
rawTool["dry_run"] = json.RawMessage(tool.DryRun)
}
tools = append(tools, rawTool)
}
products = append(products, map[string]any{"id": productID, "tools": tools})
}
payload := map[string]any{"kind": "schema", "products": products}
data, err := json.Marshal(payload)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0o600); err != nil {
t.Fatal(err)
}
}
@@ -0,0 +1,676 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package schemacompat
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
const governedSchemaCheckerSource = `package main
import (
"flag"
"fmt"
"os"
"strings"
)
func main() {
normalize := flag.String("normalize", "", "raw schema")
check := flag.String("check", "", "base schema")
current := flag.String("current", "", "candidate schema")
approved := flag.String("approved-flag-migrations", "", "base ledger")
candidate := flag.String("candidate-flag-migrations", "", "candidate ledger")
currentSnapshot := flag.String("migration-current-snapshot", "", "candidate interface snapshot")
baseSnapshot := flag.String("migration-base-snapshot", "", "base interface snapshot")
stableSnapshot := flag.String("migration-stable-snapshot", "", "stable interface snapshot")
flag.Parse()
if *normalize != "" {
data, err := os.ReadFile(*normalize)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
fmt.Print(string(data))
return
}
if *check == "" || *current == "" {
fmt.Fprintln(os.Stderr, "missing check inputs")
os.Exit(2)
}
currentData, err := os.ReadFile(*current)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if !strings.Contains(string(currentData), "COMMITTED_CANDIDATE") {
fmt.Fprintf(os.Stderr, "candidate schema did not come from detached commit: %s\n", currentData)
os.Exit(2)
}
paths := []string{*approved, *candidate, *currentSnapshot, *baseSnapshot, *stableSnapshot}
for _, path := range paths {
if path == "" {
fmt.Fprintln(os.Stderr, "governed checker did not receive all migration inputs")
os.Exit(2)
}
}
for _, fixture := range []struct {
path string
want string
}{
{*approved, "{\"version\":1,\"migrations\":[{\"authority\":\"BASE\"}]}\n"},
{*candidate, "{\"version\":1,\"migrations\":[{\"candidate\":\"COMMITTED\"}]}\n"},
{*currentSnapshot, "COMMITTED_CANDIDATE"},
{*baseSnapshot, "BASE_AUTHORITY"},
{*stableSnapshot, "STABLE_RELEASE"},
} {
data, err := os.ReadFile(fixture.path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if !strings.Contains(string(data), fixture.want) {
fmt.Fprintf(os.Stderr, "migration input %s does not contain %q: %s\n", fixture.path, fixture.want, data)
os.Exit(2)
}
}
for _, path := range []string{*currentSnapshot, *baseSnapshot, *stableSnapshot} {
data, err := os.ReadFile(path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if !strings.Contains(string(data), "INTERFACE_HELPER=BASE") {
fmt.Fprintf(os.Stderr, "interface snapshot did not use base internal helper: %s\n", data)
os.Exit(2)
}
}
fmt.Fprintln(os.Stderr, "BASE_SCHEMA_CHECKER_ENFORCED")
os.Exit(23)
}
`
const bootstrapSchemaCheckerSource = `package main
import (
"flag"
"fmt"
"os"
"strings"
)
func main() {
normalize := flag.String("normalize", "", "raw schema")
check := flag.String("check", "", "base schema")
current := flag.String("current", "", "candidate schema")
flag.Parse()
if *normalize != "" {
data, err := os.ReadFile(*normalize)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
fmt.Print(string(data))
return
}
if *check == "" || *current == "" {
fmt.Fprintln(os.Stderr, "missing check inputs")
os.Exit(2)
}
data, err := os.ReadFile(*current)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if !strings.Contains(string(data), "COMMITTED_CANDIDATE") {
fmt.Fprintf(os.Stderr, "candidate schema did not come from detached commit: %s\n", data)
os.Exit(2)
}
baselineData, err := os.ReadFile(*check)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
switch {
case strings.Contains(string(baselineData), "BASE_AUTHORITY"):
fmt.Fprintln(os.Stdout, "OLD_BASE_SCHEMA_CONTRACT_CHECKED")
case strings.Contains(string(baselineData), "STABLE_RELEASE"):
fmt.Fprintln(os.Stderr, "OLD_BASE_STABLE_SCHEMA_CHECKER_ENFORCED")
os.Exit(23)
default:
fmt.Fprintf(os.Stderr, "unexpected historical Schema contract: %s\n", baselineData)
os.Exit(2)
}
}
`
const stableSchemaGuardCheckerSource = `package main
import (
"flag"
"fmt"
"os"
"strings"
)
func main() {
normalize := flag.String("normalize", "", "raw schema")
check := flag.String("check", "", "historical schema")
current := flag.String("current", "", "candidate schema")
approved := flag.String("approved-flag-migrations", "", "base ledger")
candidate := flag.String("candidate-flag-migrations", "", "candidate ledger")
currentSnapshot := flag.String("migration-current-snapshot", "", "candidate interface snapshot")
baseSnapshot := flag.String("migration-base-snapshot", "", "base interface snapshot")
stableSnapshot := flag.String("migration-stable-snapshot", "", "stable interface snapshot")
flag.Parse()
if *normalize != "" {
data, err := os.ReadFile(*normalize)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
fmt.Print(string(data))
return
}
if *check == "" || *current == "" {
fmt.Fprintln(os.Stderr, "missing check inputs")
os.Exit(2)
}
for _, fixture := range []struct {
path string
want string
}{
{*approved, "\"authority\":\"BASE\""},
{*candidate, "\"candidate\":\"COMMITTED\""},
{*currentSnapshot, "COMMITTED_CANDIDATE"},
{*baseSnapshot, "BASE_AUTHORITY"},
{*stableSnapshot, "STABLE_RELEASE"},
} {
if fixture.path == "" {
fmt.Fprintln(os.Stderr, "stable Schema guard did not receive all migration inputs")
os.Exit(2)
}
data, err := os.ReadFile(fixture.path)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if !strings.Contains(string(data), fixture.want) {
fmt.Fprintf(os.Stderr, "stable Schema guard input %s does not contain %q: %s\n", fixture.path, fixture.want, data)
os.Exit(2)
}
}
currentData, err := os.ReadFile(*current)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
if !strings.Contains(string(currentData), "COMMITTED_CANDIDATE") {
fmt.Fprintf(os.Stderr, "candidate schema did not come from detached commit: %s\n", currentData)
os.Exit(2)
}
baselineData, err := os.ReadFile(*check)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
switch {
case strings.Contains(string(baselineData), "BASE_AUTHORITY"):
fmt.Fprintln(os.Stdout, "BASE_SCHEMA_CONTRACT_CHECKED")
case strings.Contains(string(baselineData), "STABLE_RELEASE"):
fmt.Fprintln(os.Stderr, "STABLE_SCHEMA_CONTRACT_ENFORCED")
os.Exit(23)
default:
fmt.Fprintf(os.Stderr, "unexpected historical Schema contract: %s\n", baselineData)
os.Exit(2)
}
}
`
const authorityInterfaceHelperSource = `package main
import (
"flag"
"fmt"
"os"
"example.com/dws-schema-compat-authority-test/internal/interfacesnapshot"
)
func main() {
if len(os.Args) < 2 || os.Args[1] != "generate" {
fmt.Fprintln(os.Stderr, "expected generate")
os.Exit(2)
}
flags := flag.NewFlagSet("generate", flag.ExitOnError)
output := flags.String("output", "", "snapshot output")
_ = flags.Parse(os.Args[2:])
marker, err := os.ReadFile("SNAPSHOT_MARKER")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
marker = append(marker, []byte("INTERFACE_HELPER="+interfacesnapshot.AuthorityMarker()+"\n")...)
if err := os.WriteFile(*output, marker, 0o600); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(2)
}
fmt.Printf("BASE_INTERFACE_HELPER_GENERATE=%s\n", interfacesnapshot.AuthorityMarker())
}
`
const schemaGovernedSnapshotSource = `package interfacesnapshot
import "example.com/dws-schema-compat-authority-test/internal/corecmd/runtimeannotate"
var (
_ = runtimeannotate.AnnotationFlagAliasOf
_ = runtimeannotate.AnnotationFlagAliasOrigin
_ = runtimeannotate.FlagAliasOriginCorecmdV1
)
func AuthorityMarker() string { return "BASE" }
`
const schemaBaseSnapshotSource = `package interfacesnapshot
func AuthorityMarker() string { return "BASE" }
`
const schemaStableSnapshotSource = `package interfacesnapshot
func AuthorityMarker() string { return "STABLE" }
`
const schemaCandidateSnapshotSource = `package interfacesnapshot
func AuthorityMarker() string { return "CANDIDATE" }
`
const schemaAliasContractSource = `package runtimeannotate
const (
AnnotationFlagAliasOf = "dws.compat.alias_of"
AnnotationFlagAliasOrigin = "dws.compat.alias_origin"
FlagAliasOriginCorecmdV1 = "corecmd.flag_spec_aliases.v1"
)
`
const candidateNoopSource = `package main
import "fmt"
func main() {
fmt.Println("CANDIDATE_NOOP")
}
`
func schemaBinarySource(marker string) string {
return fmt.Sprintf(`package main
import "fmt"
func main() {
fmt.Println(%q)
}
`, `{"kind":"schema","marker":"`+marker+`"}`)
}
type authorityScenario struct {
name string
baseGovernance string
candidateMutation string
checkerMode string
want string
authorityMarker string
}
func TestCrossPlatformCoverageSchemaCompatibilityUsesBaseOwnedAuthority(t *testing.T) {
tests := []authorityScenario{
{
name: "governed base rejects candidate checker and live workspace",
baseGovernance: "complete",
authorityMarker: "BASE_SCHEMA_CHECKER_ENFORCED",
},
{
name: "governed checker protects stable Schema contract",
baseGovernance: "complete",
checkerMode: "stable-schema-guard",
authorityMarker: "STABLE_SCHEMA_CONTRACT_ENFORCED",
},
{
name: "bootstrap uses old base checker and committed canonical empty ledger",
baseGovernance: "none",
authorityMarker: "OLD_BASE_STABLE_SCHEMA_CHECKER_ENFORCED",
},
{
name: "partial base fails closed",
baseGovernance: "partial",
want: "incomplete Schema flag migration governance artifact set",
},
{
name: "base ledger ancestor symlink fails closed",
baseGovernance: "ancestor-symlink",
want: "incomplete Schema flag migration governance artifact set",
},
{
name: "governed candidate must preserve schema checker",
baseGovernance: "complete",
candidateMutation: "delete-checker",
want: "must preserve the complete Schema flag migration governance artifact set",
},
{
name: "governed candidate must preserve migrations implementation",
baseGovernance: "complete",
candidateMutation: "delete-migrations",
want: "must preserve the complete Schema flag migration governance artifact set",
},
{
name: "governed candidate ledger must be a regular file",
baseGovernance: "complete",
candidateMutation: "symlink-ledger",
want: "must preserve the complete Schema flag migration governance artifact set",
},
{
name: "governed candidate helper parent must be a Git tree",
baseGovernance: "complete",
candidateMutation: "symlink-helper-parent",
want: "must preserve the complete Schema flag migration governance artifact set",
},
{
name: "candidate cannot forge alias evidence token",
baseGovernance: "complete",
candidateMutation: "forge-alias-token",
want: "may not forge framework-owned flag alias evidence",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
t.Parallel()
runSchemaAuthorityCase(t, test)
})
}
}
func runSchemaAuthorityCase(t *testing.T, test authorityScenario) {
t.Helper()
repoRoot := schemaRepositoryRoot(t)
managedRoot := filepath.Join(repoRoot, ".worktrees", "schemacompat-authority-integration")
if err := os.MkdirAll(managedRoot, 0o755); err != nil {
t.Fatalf("create managed test root: %v", err)
}
fixtureRoot, err := os.MkdirTemp(managedRoot, fmt.Sprintf("case-%d-", time.Now().UnixNano()))
if err != nil {
t.Fatalf("create managed fixture repo: %v", err)
}
t.Cleanup(func() {
if err := os.RemoveAll(fixtureRoot); err != nil {
t.Errorf("remove managed fixture repo: %v", err)
}
})
schemaCopyFile(t,
filepath.Join(repoRoot, "scripts", "policy", "check-authoritative-schema-compatibility.sh"),
filepath.Join(fixtureRoot, "scripts", "policy", "check-authoritative-schema-compatibility.sh"),
0o755,
)
schemaCopyFile(t,
filepath.Join(repoRoot, "scripts", "policy", "policy-runtime.sh"),
filepath.Join(fixtureRoot, "scripts", "policy", "policy-runtime.sh"),
0o755,
)
schemaCopyFile(t,
filepath.Join(repoRoot, "scripts", "release", "release-lib.sh"),
filepath.Join(fixtureRoot, "scripts", "release", "release-lib.sh"),
0o755,
)
schemaWriteFile(t, filepath.Join(fixtureRoot, "go.mod"), "module example.com/dws-schema-compat-authority-test\n\ngo 1.23\n", 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "cmd", "main.go"), schemaBinarySource("STABLE_RELEASE"), 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "cmd", "interface-snapshot", "main.go"), candidateNoopSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "snapshot.go"), schemaStableSnapshotSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "compare.go"), "package interfacesnapshot\n", 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "schema-compat", "main.go"), bootstrapSchemaCheckerSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "SNAPSHOT_MARKER"), "STABLE_RELEASE\n", 0o644)
schemaRun(t, fixtureRoot, "git", "init", "-b", "main")
schemaRun(t, fixtureRoot, "git", "config", "user.name", "Schema Compatibility Test")
schemaRun(t, fixtureRoot, "git", "config", "user.email", "schema-compat-test@example.invalid")
schemaRun(t, fixtureRoot, "git", "add", ".")
schemaRun(t, fixtureRoot, "git", "commit", "-m", "stable release")
stableCommit := strings.TrimSpace(schemaRun(t, fixtureRoot, "git", "rev-parse", "HEAD"))
schemaRun(t, fixtureRoot, "git", "tag", "v1.0.0", stableCommit)
schemaWriteFile(t, filepath.Join(fixtureRoot, "cmd", "main.go"), schemaBinarySource("BASE_AUTHORITY"), 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "cmd", "interface-snapshot", "main.go"), authorityInterfaceHelperSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "SNAPSHOT_MARKER"), "BASE_AUTHORITY\n", 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "snapshot.go"), schemaBaseSnapshotSource, 0o644)
switch test.baseGovernance {
case "complete":
checkerSource := governedSchemaCheckerSource
if test.checkerMode == "stable-schema-guard" {
checkerSource = stableSchemaGuardCheckerSource
}
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "schema-compat", "main.go"), checkerSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "snapshot.go"), schemaGovernedSnapshotSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "migrations.go"), "package interfacesnapshot\n", 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "corecmd", "runtimeannotate", "interface_alias.go"), schemaAliasContractSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json"), "{\"version\":1,\"migrations\":[{\"authority\":\"BASE\"}]}\n", 0o644)
case "partial":
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json"), "{\"version\":1,\"migrations\":[]}\n", 0o644)
case "ancestor-symlink":
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "schema-compat", "main.go"), governedSchemaCheckerSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "migrations.go"), "package interfacesnapshot\n", 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "corecmd", "runtimeannotate", "interface_alias.go"), "package runtimeannotate\n", 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "authority-ledger", "approved-flag-migrations-v1.json"), "{\"version\":1,\"migrations\":[]}\n", 0o644)
if err := os.Symlink("authority-ledger", filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations")); err != nil {
t.Fatalf("symlink base Schema ledger parent: %v", err)
}
case "none":
default:
t.Fatalf("unknown base governance state %q", test.baseGovernance)
}
schemaRun(t, fixtureRoot, "git", "add", ".")
schemaRun(t, fixtureRoot, "git", "commit", "-m", "base authority")
baseRef := strings.TrimSpace(schemaRun(t, fixtureRoot, "git", "rev-parse", "HEAD"))
schemaRun(t, fixtureRoot, "git", "tag", "v1.1.0", baseRef)
schemaRun(t, fixtureRoot, "git", "update-ref", "refs/tags/withdrawn/v1.1.0", baseRef)
if test.baseGovernance == "ancestor-symlink" {
ledgerPath := filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations")
if err := os.Remove(ledgerPath); err != nil {
t.Fatalf("remove inherited base ledger parent symlink: %v", err)
}
if err := os.Rename(
filepath.Join(fixtureRoot, "scripts", "policy", "authority-ledger"),
ledgerPath,
); err != nil {
t.Fatalf("restore regular candidate ledger tree: %v", err)
}
}
schemaWriteFile(t, filepath.Join(fixtureRoot, "cmd", "main.go"), schemaBinarySource("COMMITTED_CANDIDATE"), 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "cmd", "interface-snapshot", "main.go"), candidateNoopSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "schema-compat", "main.go"), candidateNoopSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "snapshot.go"), schemaCandidateSnapshotSource, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "migrations.go"), "package interfacesnapshot\n", 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "internal", "corecmd", "runtimeannotate", "interface_alias.go"), "package runtimeannotate\n", 0o644)
candidateLedger := "{\"version\":1,\"migrations\":[]}\n"
if test.baseGovernance == "complete" {
candidateLedger = "{\"version\":1,\"migrations\":[{\"candidate\":\"COMMITTED\"}]}\n"
}
schemaWriteFile(t, filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json"), candidateLedger, 0o644)
schemaWriteFile(t, filepath.Join(fixtureRoot, "SNAPSHOT_MARKER"), "COMMITTED_CANDIDATE\n", 0o644)
switch test.candidateMutation {
case "":
case "delete-checker":
if err := os.Remove(filepath.Join(fixtureRoot, "scripts", "policy", "schema-compat", "main.go")); err != nil {
t.Fatalf("remove candidate schema checker: %v", err)
}
case "delete-migrations":
if err := os.Remove(filepath.Join(fixtureRoot, "internal", "interfacesnapshot", "migrations.go")); err != nil {
t.Fatalf("remove candidate migrations implementation: %v", err)
}
case "symlink-ledger":
ledger := filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json")
if err := os.Remove(ledger); err != nil {
t.Fatalf("remove candidate ledger: %v", err)
}
if err := os.Symlink("../../../CANONICAL_EMPTY", ledger); err != nil {
t.Fatalf("symlink candidate ledger: %v", err)
}
schemaWriteFile(t, filepath.Join(fixtureRoot, "CANONICAL_EMPTY"), "{\"version\":1,\"migrations\":[]}\n", 0o644)
case "forge-alias-token":
schemaWriteFile(t,
filepath.Join(fixtureRoot, "internal", "evil", "forged.go"),
"package evil\n\nconst forged = \"dws.compat.alias_of\"\n",
0o644,
)
case "symlink-helper-parent":
helperPath := filepath.Join(fixtureRoot, "internal", "interfacesnapshot")
alternatePath := filepath.Join(fixtureRoot, "internal", "interfacesnapshot-alt")
if err := os.Rename(helperPath, alternatePath); err != nil {
t.Fatalf("move candidate Schema helper behind ancestor symlink: %v", err)
}
if err := os.Symlink("interfacesnapshot-alt", helperPath); err != nil {
t.Fatalf("symlink candidate Schema helper parent: %v", err)
}
default:
t.Fatalf("unknown candidate mutation %q", test.candidateMutation)
}
schemaRun(t, fixtureRoot, "git", "add", ".")
schemaRun(t, fixtureRoot, "git", "commit", "-m", "candidate revision")
candidateRef := strings.TrimSpace(schemaRun(t, fixtureRoot, "git", "rev-parse", "HEAD"))
// 让调用者工作区前进到另一提交,并放入可自批的二进制与 ledger;显式
// candidate ref 必须隔离这些内容。
schemaWriteFile(t, filepath.Join(fixtureRoot, "cmd", "main.go"), schemaBinarySource("LIVE_WORKTREE_BINARY"), 0o644)
liveLedger := filepath.Join(fixtureRoot, "scripts", "policy", "interface-migrations", "approved-flag-migrations-v1.json")
if err := os.Remove(liveLedger); err != nil && !os.IsNotExist(err) {
t.Fatalf("remove selected candidate ledger from live revision: %v", err)
}
schemaWriteFile(t, liveLedger, "{\"version\":1,\"migrations\":[{\"live\":true}]}\n", 0o644)
schemaRun(t, fixtureRoot, "git", "add", ".")
schemaRun(t, fixtureRoot, "git", "commit", "-m", "unselected live revision")
schemaWriteFile(t, filepath.Join(fixtureRoot, "dws"), "#!/bin/sh\nprintf '%s\\n' 'LIVE_WORKTREE_BINARY'\n", 0o755)
cmd := exec.Command(
"sh",
"scripts/policy/check-authoritative-schema-compatibility.sh",
"--base-ref", baseRef,
"--stable-ref", "v1.0.0",
"--candidate-ref", candidateRef,
)
cmd.Dir = fixtureRoot
cmd.Env = schemaIntegrationEnvironment(fixtureRoot)
output, runErr := cmd.CombinedOutput()
got := string(output)
if runErr == nil {
t.Fatalf("Schema compatibility script unexpectedly succeeded; output:\n%s", got)
}
for _, forbidden := range []string{"CANDIDATE_NOOP", "LIVE_WORKTREE_BINARY"} {
if strings.Contains(got, forbidden) {
t.Fatalf("candidate-controlled authority %q took effect; output:\n%s", forbidden, got)
}
}
if test.authorityMarker != "" {
if !strings.Contains(got, test.authorityMarker) {
t.Fatalf("Schema compatibility script did not use base authority %q; output:\n%s", test.authorityMarker, got)
}
if test.baseGovernance == "complete" && strings.Count(got, "BASE_INTERFACE_HELPER_GENERATE=BASE") != 3 {
t.Fatalf("governed Schema check did not generate three base-owned interface snapshots; output:\n%s", got)
}
wrongStable := exec.Command(
"sh",
"scripts/policy/check-authoritative-schema-compatibility.sh",
"--base-ref", baseRef,
"--stable-ref", "HEAD",
"--candidate-ref", candidateRef,
)
wrongStable.Dir = fixtureRoot
wrongStable.Env = schemaIntegrationEnvironment(fixtureRoot)
wrongOutput, wrongErr := wrongStable.CombinedOutput()
if wrongErr == nil || !strings.Contains(string(wrongOutput), "is not the expected highest GA tag") {
t.Fatalf("arbitrary Schema stable ref was not rejected: err=%v output:\n%s", wrongErr, wrongOutput)
}
return
}
if !strings.Contains(got, test.want) {
t.Fatalf("Schema compatibility script output missing %q:\n%s", test.want, got)
}
}
func schemaRepositoryRoot(t *testing.T) string {
t.Helper()
_, sourceFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("resolve test source path")
}
root := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "..", "..", ".."))
if _, err := os.Stat(filepath.Join(root, "scripts", "policy", "check-authoritative-schema-compatibility.sh")); err != nil {
t.Fatalf("resolve repository root %s: %v", root, err)
}
return root
}
func schemaCopyFile(t *testing.T, source, destination string, mode os.FileMode) {
t.Helper()
content, err := os.ReadFile(source)
if err != nil {
t.Fatalf("read %s: %v", source, err)
}
schemaWriteFile(t, destination, string(content), mode)
}
func schemaWriteFile(t *testing.T, path, content string, mode os.FileMode) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("create parent for %s: %v", path, err)
}
if err := os.WriteFile(path, []byte(content), mode); err != nil {
t.Fatalf("write %s: %v", path, err)
}
}
func schemaRun(t *testing.T, dir, name string, args ...string) string {
t.Helper()
cmd := exec.Command(name, args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("%s %s failed: %v\n%s", name, strings.Join(args, " "), err, output)
}
return string(output)
}
func schemaIntegrationEnvironment(fixtureRoot string) []string {
overrides := map[string]string{
"DWS_BIN": filepath.Join(fixtureRoot, "dws"),
"DWS_POLICY_TMPDIR": "",
"GOCACHE": filepath.Join(fixtureRoot, ".cache", "go-build"),
"GOMODCACHE": filepath.Join(fixtureRoot, ".cache", "go-mod"),
"GOTMPDIR": "",
"GOTOOLCHAIN": "local",
"GOWORK": "off",
}
environment := make([]string, 0, len(os.Environ())+len(overrides))
for _, item := range os.Environ() {
key, _, _ := strings.Cut(item, "=")
if _, replaced := overrides[key]; !replaced {
environment = append(environment, item)
}
}
for key, value := range overrides {
environment = append(environment, key+"="+value)
}
return environment
}
+143 -4
View File
@@ -401,15 +401,154 @@ func TestInterfaceIntegrityWorkflowContract(t *testing.T) {
}
interfaceJob := workflow[interfaceStart:interfaceEnd]
for _, want := range []string{
"name: Check complete CLI command compatibility",
"./scripts/policy/check-command-compatibility.sh",
`--base-ref "$COMPATIBILITY_BASE_REF"`,
`--stable-ref "$COMPATIBILITY_STABLE_REF"`,
"name: Check historical commands, help, and complete CLI compatibility",
"make authoritative-interface-integrity",
`BASE_REF="$COMPATIBILITY_BASE_REF"`,
`STABLE_REF="$COMPATIBILITY_STABLE_REF"`,
`CANDIDATE_REF="$COMPATIBILITY_CANDIDATE_REF"`,
} {
if !strings.Contains(interfaceJob, want) {
t.Errorf("Interface Integrity job missing release-equivalent compatibility contract %q", want)
}
}
for _, want := range []string{
`candidate_ref="$(git rev-parse 'HEAD^{commit}')"`,
`[ "$candidate_ref" != "$PR_HEAD_SHA" ]`,
`"COMPATIBILITY_BASE_REF=$base_ref"`,
`"COMPATIBILITY_STABLE_REF=$stable_ref"`,
`"COMPATIBILITY_CANDIDATE_REF=$candidate_ref" >> "$GITHUB_ENV"`,
} {
if got := strings.Count(interfaceJob, want); got != 1 {
t.Errorf("Interface Integrity job contract %q count = %d, want exactly one definition", want, got)
}
}
if got := strings.Count(interfaceJob, `>> "$GITHUB_ENV"`); got != 1 {
t.Errorf("Interface Integrity job must append its compatibility refs to GITHUB_ENV once, got %d writes", got)
}
if strings.Count(interfaceJob, "make authoritative-interface-integrity") != 1 {
t.Errorf("Interface Integrity job must have exactly one compatibility decision seam")
}
if strings.Contains(interfaceJob, "./scripts/policy/check-command-compatibility.sh") {
t.Error("Interface Integrity job must not bypass the authoritative Make seam")
}
if strings.Contains(interfaceJob, "check-interface-baseline.sh") {
t.Error("Interface Integrity job must not reintroduce the legacy fixture checker")
}
schemaStart := strings.Index(interfaceJob, "\n - name: Check complete Schema compatibility\n")
if schemaStart < 0 {
t.Fatal("Interface Integrity job missing complete Schema compatibility step")
}
schemaRemainder := interfaceJob[schemaStart+1:]
schemaEnd := strings.Index(schemaRemainder, "\n - name:")
if schemaEnd < 0 {
t.Fatal("complete Schema compatibility step has no workflow boundary")
}
schemaStep := schemaRemainder[:schemaEnd]
for _, want := range []string{
"make schema-compatibility",
`BASE_REF="$COMPATIBILITY_BASE_REF"`,
`STABLE_REF="$COMPATIBILITY_STABLE_REF"`,
`CANDIDATE_REF="$COMPATIBILITY_CANDIDATE_REF"`,
} {
if !strings.Contains(schemaStep, want) {
t.Errorf("Schema compatibility step missing authoritative contract %q", want)
}
}
}
func TestLocalInterfaceIntegrityUsesAuthoritativeSeam(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
data, err := os.ReadFile(filepath.Join(root, "Makefile"))
if err != nil {
t.Fatalf("ReadFile(Makefile) error = %v", err)
}
makefile := string(data)
interfaceStart := strings.Index(makefile, "\ninterface-integrity:\n")
authoritativeStart := strings.Index(makefile, "\nauthoritative-interface-integrity:\n")
coverageStart := strings.Index(makefile, "\ncoverage-gate:\n")
if interfaceStart < 0 || authoritativeStart <= interfaceStart || coverageStart <= authoritativeStart {
t.Fatal("Makefile missing interface-integrity target boundaries")
}
interfaceTarget := makefile[interfaceStart:authoritativeStart]
for _, want := range []string{
"./scripts/policy/check-authoritative-interface-baselines.sh",
`base_ref="origin/main"`,
`candidate_ref="HEAD"`,
`--base-ref "$$base_ref"`,
`--stable-ref "$(STABLE_REF)"`,
`--candidate-ref "$$candidate_ref"`,
} {
if !strings.Contains(interfaceTarget, want) {
t.Errorf("interface-integrity target missing authoritative contract %q", want)
}
}
if strings.Contains(interfaceTarget, "check-interface-baseline.sh") {
t.Error("interface-integrity target still invokes the legacy fixture checker")
}
authoritativeTarget := makefile[authoritativeStart:coverageStart]
for _, want := range []string{
"./scripts/policy/check-authoritative-interface-baselines.sh",
`candidate_ref="HEAD"`,
`--base-ref "$(BASE_REF)"`,
`--stable-ref "$(STABLE_REF)"`,
`--candidate-ref "$$candidate_ref"`,
} {
if !strings.Contains(authoritativeTarget, want) {
t.Errorf("authoritative-interface-integrity target missing contract %q", want)
}
}
if got := strings.Count(makefile, "./scripts/policy/check-interface-baseline.sh"); got != 2 {
t.Fatalf("legacy fixture helper invocation count = %d, want update/reset only", got)
}
for _, target := range []string{"update-interface-baseline", "reset-interface-baseline"} {
marker := "\n" + target + ":\n"
start := strings.Index(makefile, marker)
if start < 0 {
t.Fatalf("Makefile missing %s target", target)
}
end := strings.Index(makefile[start+len(marker):], "\n\n")
if end < 0 {
t.Fatalf("Makefile %s target has no boundary", target)
}
block := makefile[start : start+len(marker)+end]
if !strings.Contains(block, "./scripts/policy/check-interface-baseline.sh") {
t.Errorf("%s target must retain the legacy fixture helper", target)
}
}
}
func TestLocalSchemaCompatibilityUsesAuthoritativeCandidateSeam(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("Abs(repo root) error = %v", err)
}
data, err := os.ReadFile(filepath.Join(root, "Makefile"))
if err != nil {
t.Fatalf("ReadFile(Makefile) error = %v", err)
}
makefile := string(data)
schemaStart := strings.Index(makefile, "\nschema-compatibility:\n")
skillStart := strings.Index(makefile, "\nskill-command-integrity:\n")
if schemaStart < 0 || skillStart <= schemaStart {
t.Fatal("Makefile missing schema-compatibility target boundaries")
}
schemaTarget := makefile[schemaStart:skillStart]
for _, want := range []string{
"./scripts/policy/check-authoritative-schema-compatibility.sh",
`candidate_ref="HEAD"`,
`--base-ref "$(BASE_REF)"`,
`--stable-ref "$(STABLE_REF)"`,
`--candidate-ref "$$candidate_ref"`,
} {
if !strings.Contains(schemaTarget, want) {
t.Errorf("schema-compatibility target missing authoritative contract %q", want)
}
}
}
func TestInterfaceSensitiveClassificationCoversCoreCommandFramework(t *testing.T) {
@@ -0,0 +1,69 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package scripts_test
import (
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
)
func TestFrameworkOwnsInterfaceFlagAliasEvidence(t *testing.T) {
root, err := filepath.Abs(filepath.Join("..", ".."))
if err != nil {
t.Fatalf("resolve repository root: %v", err)
}
allowed := map[string]bool{
"internal/corecmd/corecmd.go": true,
"internal/corecmd/runtimeannotate/interface_alias.go": true,
"internal/interfacesnapshot/snapshot.go": true,
}
protectedTokens := []string{
"dws.compat.alias_of",
"dws.compat.alias_origin",
"corecmd.flag_spec_aliases.v1",
"AnnotationFlagAliasOf",
"AnnotationFlagAliasOrigin",
"FlagAliasOriginCorecmdV1",
}
err = filepath.WalkDir(root, func(path string, entry fs.DirEntry, walkErr error) error {
if walkErr != nil {
return walkErr
}
if entry.IsDir() {
switch entry.Name() {
case ".git", ".worktrees", "vendor":
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
relative, err := filepath.Rel(root, path)
if err != nil {
return err
}
relative = filepath.ToSlash(relative)
if allowed[relative] {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return err
}
for _, token := range protectedTokens {
if strings.Contains(string(content), token) {
t.Errorf("production file %s uses framework-owned flag alias evidence token %q", relative, token)
}
}
return nil
})
if err != nil {
t.Fatalf("scan production Go files: %v", err)
}
}