1566 lines
66 KiB
Go
1566 lines
66 KiB
Go
// Copyright 2026 Alibaba Group
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot"
|
|
)
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationsAuthorizeOnlyExactProjection(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
current := schemaCommandMigrationContract(true)
|
|
migrations := schemaCommandMigrationAuthorizations()
|
|
normalized, err := normalizeSchemaCommandMigrations(baseline, current, migrations)
|
|
if err != nil {
|
|
t.Fatalf("normalize exact command migrations: %v", err)
|
|
}
|
|
if failures := checkCompatibility(normalized, current); len(failures) != 0 {
|
|
t.Fatalf("exact command migrations remained incompatible: %v", failures)
|
|
}
|
|
|
|
unrelated := cloneContract(current)
|
|
product := unrelated.Products["chat"]
|
|
tool := product.Tools["chat.move"]
|
|
delete(tool.Parameters, "keep")
|
|
product.Tools["chat.move"] = tool
|
|
unrelated.Products["chat"] = product
|
|
normalized, err = normalizeSchemaCommandMigrations(baseline, unrelated, migrations)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if failures := strings.Join(checkCompatibility(normalized, unrelated), "\n"); !strings.Contains(failures, `lost parameter "keep"`) {
|
|
t.Fatalf("unrelated parameter removal was hidden: %s", failures)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationsFailClosedOnDrift(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
migrations := schemaCommandMigrationAuthorizations()
|
|
|
|
parameterDrift := schemaCommandMigrationContract(true)
|
|
product := parameterDrift.Products["chat"]
|
|
tool := product.Tools["chat.move"]
|
|
parameter := tool.Parameters["new-id"]
|
|
parameter.Property = "differentProperty"
|
|
tool.Parameters["new-id"] = parameter
|
|
product.Tools["chat.move"] = tool
|
|
parameterDrift.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, parameterDrift, migrations); err == nil || !strings.Contains(err.Error(), "changed a non-name field") {
|
|
t.Fatalf("parameter drift error=%v", err)
|
|
}
|
|
|
|
safetyDrift := schemaCommandMigrationContract(true)
|
|
product = safetyDrift.Products["chat"]
|
|
replacement := product.Tools["chat.create_topic"]
|
|
replacement.Risk = "high"
|
|
product.Tools["chat.create_topic"] = replacement
|
|
safetyDrift.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, safetyDrift, migrations); err == nil || !strings.Contains(err.Error(), "changed interface or safety identity") {
|
|
t.Fatalf("extraction safety drift error=%v", err)
|
|
}
|
|
|
|
missingReplacement := schemaCommandMigrationContract(true)
|
|
product = missingReplacement.Products["chat"]
|
|
delete(product.Tools, "chat.create_topic")
|
|
missingReplacement.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, missingReplacement, migrations); err == nil || !strings.Contains(err.Error(), "lacks replacement Schema tool") {
|
|
t.Fatalf("missing replacement error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationRejectsUnregisteredRequiredParameters(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
migrations := schemaCommandMigrationAuthorizations()
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
parameter parameterSchema
|
|
}{
|
|
{name: "required", parameter: parameterSchema{Type: `"string"`, Property: "must", Required: true}},
|
|
{name: "cli_required", parameter: parameterSchema{Type: `"string"`, Property: "must", CLIRequired: true}},
|
|
{name: "required_when", parameter: parameterSchema{Type: `"string"`, Property: "must", RequiredWhen: "mode=forced"}},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
current := schemaCommandMigrationContract(true)
|
|
product := current.Products["chat"]
|
|
tool := product.Tools["chat.move"]
|
|
tool.Parameters["must"] = test.parameter
|
|
product.Tools["chat.move"] = tool
|
|
current.Products["chat"] = product
|
|
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, current, migrations); err == nil ||
|
|
!strings.Contains(err.Error(), `introduced unregistered required Schema parameter "must"`) {
|
|
t.Fatalf("unregistered required parameter error=%v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
current := schemaCommandMigrationContract(true)
|
|
product := current.Products["chat"]
|
|
tool := product.Tools["chat.move"]
|
|
tool.Parameters["optional"] = parameterSchema{Type: `"string"`, Property: "optional"}
|
|
product.Tools["chat.move"] = tool
|
|
current.Products["chat"] = product
|
|
normalized, err := normalizeSchemaCommandMigrations(baseline, current, migrations)
|
|
if err != nil {
|
|
t.Fatalf("optional addition should remain compatible: %v", err)
|
|
}
|
|
if failures := checkCompatibility(normalized, current); len(failures) != 0 {
|
|
t.Fatalf("optional addition should remain compatible: %v", failures)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationRejectsParameterTargetCollision(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
current := schemaCommandMigrationContract(true)
|
|
migrations := schemaCommandMigrationAuthorizations()
|
|
migrations[0].Schema.Parameters = []interfacesnapshot.CommandParameterMigration{{From: "old-id", To: "keep"}}
|
|
|
|
product := current.Products["chat"]
|
|
tool := product.Tools["chat.move"]
|
|
delete(tool.Parameters, "new-id")
|
|
tool.Parameters["keep"] = baseline.Products["chat"].Tools["chat.move"].Parameters["old-id"]
|
|
tool.Constraints = baseline.Products["chat"].Tools["chat.move"].Constraints
|
|
product.Tools["chat.move"] = tool
|
|
current.Products["chat"] = product
|
|
|
|
normalized, err := normalizeSchemaCommandMigrations(baseline, current, migrations)
|
|
if err == nil {
|
|
if failures := checkCompatibility(normalized, current); len(failures) != 0 {
|
|
t.Fatalf("hostile target-collision fixture should otherwise pass compatibility: %v", failures)
|
|
}
|
|
t.Fatal("parameter target collision was accepted")
|
|
}
|
|
if !strings.Contains(err.Error(), `target "keep" already exists in historical Schema tool`) {
|
|
t.Fatalf("parameter target collision error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationNormalizationEdges(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
current := schemaCommandMigrationContract(true)
|
|
migrations := schemaCommandMigrationAuthorizations()
|
|
|
|
missingHistorical := cloneContract(baseline)
|
|
product := missingHistorical.Products["chat"]
|
|
delete(product.Tools, "chat.move")
|
|
missingHistorical.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(missingHistorical, current, migrations); err != nil {
|
|
t.Fatalf("missing historical tool should be a no-op: %v", err)
|
|
}
|
|
|
|
missingCurrent := cloneContract(current)
|
|
product = missingCurrent.Products["chat"]
|
|
delete(product.Tools, "chat.move")
|
|
missingCurrent.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, missingCurrent, migrations); err != nil {
|
|
t.Fatalf("missing current source should remain for ordinary checker: %v", err)
|
|
}
|
|
|
|
alreadyAfter := schemaCommandMigrationContract(true)
|
|
if _, err := normalizeSchemaCommandMigrations(alreadyAfter, current, migrations[:1]); err != nil {
|
|
t.Fatalf("already-after baseline should be a no-op: %v", err)
|
|
}
|
|
|
|
wrongHistoricalPath := cloneContract(baseline)
|
|
product = wrongHistoricalPath.Products["chat"]
|
|
tool := product.Tools["chat.move"]
|
|
tool.PrimaryCLIPath = "chat unrelated"
|
|
product.Tools["chat.move"] = tool
|
|
wrongHistoricalPath.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(wrongHistoricalPath, current, migrations); err == nil || !strings.Contains(err.Error(), "historical Schema tool") {
|
|
t.Fatalf("wrong historical path error=%v", err)
|
|
}
|
|
|
|
wrongCurrentPath := cloneContract(current)
|
|
product = wrongCurrentPath.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
tool.PrimaryCLIPath = "chat unrelated"
|
|
product.Tools["chat.move"] = tool
|
|
wrongCurrentPath.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, wrongCurrentPath, migrations); err != nil {
|
|
t.Fatalf("wrong current path should remain for ordinary checker: %v", err)
|
|
}
|
|
|
|
missingHistoricalParameter := cloneContract(baseline)
|
|
product = missingHistoricalParameter.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
delete(tool.Parameters, "old-id")
|
|
product.Tools["chat.move"] = tool
|
|
missingHistoricalParameter.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(missingHistoricalParameter, current, migrations); err == nil || !strings.Contains(err.Error(), "lacks parameter") {
|
|
t.Fatalf("missing historical parameter error=%v", err)
|
|
}
|
|
|
|
legacyPublished := cloneContract(current)
|
|
product = legacyPublished.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
tool.Parameters["old-id"] = baseline.Products["chat"].Tools["chat.move"].Parameters["old-id"]
|
|
product.Tools["chat.move"] = tool
|
|
legacyPublished.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, legacyPublished, migrations); err == nil || !strings.Contains(err.Error(), "still publishes legacy") {
|
|
t.Fatalf("legacy parameter error=%v", err)
|
|
}
|
|
|
|
missingReplacementParameter := cloneContract(current)
|
|
product = missingReplacementParameter.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
delete(tool.Parameters, "new-id")
|
|
product.Tools["chat.move"] = tool
|
|
missingReplacementParameter.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, missingReplacementParameter, migrations); err == nil || !strings.Contains(err.Error(), "does not publish replacement") {
|
|
t.Fatalf("missing replacement parameter error=%v", err)
|
|
}
|
|
|
|
constraintDrift := cloneContract(current)
|
|
product = constraintDrift.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
tool.Constraints = `{"require_one_of":[["new-id"]]}`
|
|
product.Tools["chat.move"] = tool
|
|
constraintDrift.Products["chat"] = product
|
|
normalized, err := normalizeSchemaCommandMigrations(baseline, constraintDrift, migrations)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if failures := strings.Join(checkCompatibility(normalized, constraintDrift), "\n"); !strings.Contains(failures, "changed constraints") {
|
|
t.Fatalf("constraint drift was hidden: %s", failures)
|
|
}
|
|
|
|
unchangedLegacyConstraints := cloneContract(current)
|
|
product = unchangedLegacyConstraints.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
tool.Constraints = baseline.Products["chat"].Tools["chat.move"].Constraints
|
|
product.Tools["chat.move"] = tool
|
|
unchangedLegacyConstraints.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, unchangedLegacyConstraints, migrations); err == nil ||
|
|
!strings.Contains(err.Error(), "still reference legacy Schema constraint parameter") {
|
|
t.Fatalf("unchanged legacy constraints error=%v", err)
|
|
}
|
|
|
|
mixedLegacyConstraints := cloneContract(current)
|
|
product = mixedLegacyConstraints.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
tool.Constraints = `{"require_together":[["keep","new-id","old-id"]]}`
|
|
product.Tools["chat.move"] = tool
|
|
mixedLegacyConstraints.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, mixedLegacyConstraints, migrations); err == nil ||
|
|
!strings.Contains(err.Error(), "still reference legacy Schema constraint parameter") {
|
|
t.Fatalf("mixed legacy constraints error=%v", err)
|
|
}
|
|
|
|
malformedHistoricalConstraints := cloneContract(baseline)
|
|
product = malformedHistoricalConstraints.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
tool.Constraints = "{"
|
|
product.Tools["chat.move"] = tool
|
|
malformedHistoricalConstraints.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(malformedHistoricalConstraints, current, migrations); err == nil ||
|
|
!strings.Contains(err.Error(), "historical Schema constraints are not canonicalizable") {
|
|
t.Fatalf("malformed historical constraints error=%v", err)
|
|
}
|
|
|
|
malformedCurrentConstraints := cloneContract(current)
|
|
product = malformedCurrentConstraints.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
tool.Constraints = "{"
|
|
product.Tools["chat.move"] = tool
|
|
malformedCurrentConstraints.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, malformedCurrentConstraints, migrations); err == nil ||
|
|
!strings.Contains(err.Error(), "current Schema constraints are not canonicalizable") {
|
|
t.Fatalf("malformed current constraints error=%v", err)
|
|
}
|
|
if source, found := migratedConstraintSourceParameter("{", map[string]string{"legacy": "canonical"}); found || source != "" {
|
|
t.Fatalf("malformed migrated constraint source = %q, %v", source, found)
|
|
}
|
|
|
|
extractionWrongSource := cloneContract(current)
|
|
product = extractionWrongSource.Products["chat"]
|
|
tool = product.Tools["chat.create_group"]
|
|
tool.PrimaryCLIPath = "chat unrelated"
|
|
product.Tools["chat.create_group"] = tool
|
|
extractionWrongSource.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, extractionWrongSource, migrations[1:]); err != nil {
|
|
t.Fatalf("wrong extraction source path should remain for ordinary checker: %v", err)
|
|
}
|
|
|
|
extractionHistoricalMissing := cloneContract(baseline)
|
|
product = extractionHistoricalMissing.Products["chat"]
|
|
tool = product.Tools["chat.create_group"]
|
|
delete(tool.Parameters, "thread")
|
|
product.Tools["chat.create_group"] = tool
|
|
extractionHistoricalMissing.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(extractionHistoricalMissing, current, migrations[1:]); err == nil || !strings.Contains(err.Error(), "historical Schema tool lacks") {
|
|
t.Fatalf("missing extracted historical parameter error=%v", err)
|
|
}
|
|
|
|
extractionStillPublished := cloneContract(current)
|
|
product = extractionStillPublished.Products["chat"]
|
|
tool = product.Tools["chat.create_group"]
|
|
tool.Parameters["thread"] = baseline.Products["chat"].Tools["chat.create_group"].Parameters["thread"]
|
|
product.Tools["chat.create_group"] = tool
|
|
extractionStillPublished.Products["chat"] = product
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, extractionStillPublished, migrations[1:]); err == nil || !strings.Contains(err.Error(), "still publishes extracted") {
|
|
t.Fatalf("still-published extraction error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaFlagExtractionRequiresCompleteReplacementProjection(t *testing.T) {
|
|
t.Run("missing historical mapping", func(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
current := schemaCommandMigrationContract(true)
|
|
migrations := schemaCommandMigrationAuthorizations()
|
|
parameters := make([]interfacesnapshot.CommandParameterMigration, 0, len(migrations[1].Schema.Parameters)-1)
|
|
for _, parameter := range migrations[1].Schema.Parameters {
|
|
if parameter.From != "users" {
|
|
parameters = append(parameters, parameter)
|
|
}
|
|
}
|
|
migrations[1].Schema.Parameters = parameters
|
|
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, current, migrations); err == nil ||
|
|
!strings.Contains(err.Error(), `does not map historical Schema parameter "users"`) {
|
|
t.Fatalf("missing historical mapping error=%v", err)
|
|
}
|
|
})
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
mutate func([]interfacesnapshot.CommandMigration)
|
|
want string
|
|
}{
|
|
{
|
|
name: "duplicate historical mapping",
|
|
mutate: func(migrations []interfacesnapshot.CommandMigration) {
|
|
migrations[1].Schema.Parameters = append(
|
|
migrations[1].Schema.Parameters,
|
|
migrations[1].Schema.Parameters[0],
|
|
)
|
|
},
|
|
want: "maps historical Schema parameter \"name\" more than once",
|
|
},
|
|
{
|
|
name: "legacy mapping has two targets",
|
|
mutate: func(migrations []interfacesnapshot.CommandMigration) {
|
|
migrations[1].Schema.Parameters[3].To = "thread"
|
|
},
|
|
want: "must map to one replacement constant",
|
|
},
|
|
{
|
|
name: "shared mapping has no target",
|
|
mutate: func(migrations []interfacesnapshot.CommandMigration) {
|
|
migrations[1].Schema.Parameters[0].To = ""
|
|
},
|
|
want: "must map to one replacement parameter",
|
|
},
|
|
{
|
|
name: "duplicate replacement target",
|
|
mutate: func(migrations []interfacesnapshot.CommandMigration) {
|
|
migrations[1].Schema.Parameters[1].To = "name"
|
|
},
|
|
want: "is mapped from both \"name\" and \"type\"",
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
current := schemaCommandMigrationContract(true)
|
|
migrations := schemaCommandMigrationAuthorizations()
|
|
test.mutate(migrations)
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, current, migrations); err == nil ||
|
|
!strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("invalid mapping error=%v, want %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("replacement removes historical dry run", func(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
mutateSchemaCommandMigrationTool(&baseline, "chat.create_group", func(tool *toolSchema) {
|
|
tool.DryRun = `{"mode":"native"}`
|
|
})
|
|
current := schemaCommandMigrationContract(true)
|
|
mutateSchemaCommandMigrationTool(¤t, "chat.create_group", func(tool *toolSchema) {
|
|
tool.DryRun = `{"mode":"native"}`
|
|
})
|
|
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, current, schemaCommandMigrationAuthorizations()); err == nil ||
|
|
!strings.Contains(err.Error(), "changed or removed dry_run") {
|
|
t.Fatalf("replacement dry_run error=%v, want monotonic preservation rejection", err)
|
|
}
|
|
})
|
|
|
|
t.Run("replacement adds dry run", func(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
current := schemaCommandMigrationContract(true)
|
|
mutateSchemaCommandMigrationTool(¤t, "chat.create_topic", func(tool *toolSchema) {
|
|
tool.DryRun = `{"mode":"native"}`
|
|
})
|
|
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, current, schemaCommandMigrationAuthorizations()); err != nil {
|
|
t.Fatalf("additive replacement dry_run was rejected: %v", err)
|
|
}
|
|
})
|
|
|
|
for _, test := range []struct {
|
|
name string
|
|
mutate func(*schemaContract)
|
|
want string
|
|
}{
|
|
{
|
|
name: "missing shared parameter",
|
|
mutate: func(current *schemaContract) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.create_topic", func(tool *toolSchema) {
|
|
delete(tool.Parameters, "users")
|
|
})
|
|
},
|
|
want: `does not publish mapped Schema parameter "users"`,
|
|
},
|
|
{
|
|
name: "shared parameter field drift",
|
|
mutate: func(current *schemaContract) {
|
|
mutateSchemaCommandMigrationParameter(current, "chat.create_topic", "users", func(parameter *parameterSchema) {
|
|
parameter.Property = "differentUserIds"
|
|
})
|
|
},
|
|
want: "changed a non-name field",
|
|
},
|
|
{
|
|
name: "extra replacement parameter",
|
|
mutate: func(current *schemaContract) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.create_topic", func(tool *toolSchema) {
|
|
tool.Parameters["extra"] = parameterSchema{Type: `"string"`, Property: "extra"}
|
|
})
|
|
},
|
|
want: `publishes unmapped Schema parameter "extra"`,
|
|
},
|
|
{
|
|
name: "constraints drift",
|
|
mutate: func(current *schemaContract) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.create_topic", func(tool *toolSchema) {
|
|
tool.Constraints = `{"require_one_of":[["name","type"]]}`
|
|
})
|
|
},
|
|
want: "changed constraints",
|
|
},
|
|
{
|
|
name: "positionals drift",
|
|
mutate: func(current *schemaContract) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.create_topic", func(tool *toolSchema) {
|
|
tool.Positionals[0].Name = "topic-name"
|
|
})
|
|
},
|
|
want: "changed positionals",
|
|
},
|
|
{
|
|
name: "positionals length drift",
|
|
mutate: func(current *schemaContract) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.create_topic", func(tool *toolSchema) {
|
|
tool.Positionals = append(tool.Positionals, positionalSchema{Name: "type", Index: 1, Type: "string"})
|
|
})
|
|
},
|
|
want: "changed positionals",
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
current := schemaCommandMigrationContract(true)
|
|
test.mutate(¤t)
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, current, schemaCommandMigrationAuthorizations()); err == nil ||
|
|
!strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("replacement projection error=%v, want %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaFlagExtractionValidatesBooleanConstant(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
mutate func(*schemaContract, *schemaContract, []interfacesnapshot.CommandMigration)
|
|
want string
|
|
}{
|
|
{
|
|
name: "constant property mismatch",
|
|
mutate: func(_, _ *schemaContract, migrations []interfacesnapshot.CommandMigration) {
|
|
migrations[1].Schema.Parameters[3].ReplacementConstant.Property = "differentProperty"
|
|
},
|
|
want: "constant property",
|
|
},
|
|
{
|
|
name: "constant false",
|
|
mutate: func(_, _ *schemaContract, migrations []interfacesnapshot.CommandMigration) {
|
|
migrations[1].Schema.Parameters[3].ReplacementConstant.Value = false
|
|
},
|
|
want: "constant true",
|
|
},
|
|
{
|
|
name: "legacy type",
|
|
mutate: func(baseline, _ *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(baseline, "chat.create_group", "thread", func(parameter *parameterSchema) {
|
|
parameter.Type = `"string"`
|
|
})
|
|
},
|
|
want: "must have boolean type",
|
|
},
|
|
{
|
|
name: "legacy required",
|
|
mutate: func(baseline, _ *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(baseline, "chat.create_group", "thread", func(parameter *parameterSchema) {
|
|
parameter.Required = true
|
|
})
|
|
},
|
|
want: "must remain optional",
|
|
},
|
|
{
|
|
name: "legacy cli required",
|
|
mutate: func(baseline, _ *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(baseline, "chat.create_group", "thread", func(parameter *parameterSchema) {
|
|
parameter.CLIRequired = true
|
|
})
|
|
},
|
|
want: "must remain optional",
|
|
},
|
|
{
|
|
name: "legacy required when",
|
|
mutate: func(baseline, _ *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(baseline, "chat.create_group", "thread", func(parameter *parameterSchema) {
|
|
parameter.RequiredWhen = "mode=topic"
|
|
})
|
|
},
|
|
want: "must not declare required_when",
|
|
},
|
|
{
|
|
name: "legacy default true",
|
|
mutate: func(baseline, _ *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(baseline, "chat.create_group", "thread", func(parameter *parameterSchema) {
|
|
parameter.Default = "true"
|
|
})
|
|
},
|
|
want: "default must be absent or false",
|
|
},
|
|
{
|
|
name: "legacy interface default true",
|
|
mutate: func(baseline, _ *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(baseline, "chat.create_group", "thread", func(parameter *parameterSchema) {
|
|
parameter.InterfaceDefault = "true"
|
|
})
|
|
},
|
|
want: "interface_default must be absent or false",
|
|
},
|
|
{
|
|
name: "legacy interface type",
|
|
mutate: func(baseline, _ *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(baseline, "chat.create_group", "thread", func(parameter *parameterSchema) {
|
|
parameter.InterfaceType = "string"
|
|
})
|
|
},
|
|
want: "interface_type must be empty or boolean",
|
|
},
|
|
{
|
|
name: "legacy enum excludes true",
|
|
mutate: func(baseline, _ *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(baseline, "chat.create_group", "thread", func(parameter *parameterSchema) {
|
|
parameter.Enum = []string{"false"}
|
|
})
|
|
},
|
|
want: "enum must allow true",
|
|
},
|
|
{
|
|
name: "legacy format",
|
|
mutate: func(baseline, _ *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(baseline, "chat.create_group", "thread", func(parameter *parameterSchema) {
|
|
parameter.Format = "custom-bool"
|
|
})
|
|
},
|
|
want: "format must be empty",
|
|
},
|
|
{
|
|
name: "replacement publishes constant property",
|
|
mutate: func(_, current *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.create_topic", func(tool *toolSchema) {
|
|
tool.Parameters["thread-copy"] = parameterSchema{Type: `"boolean"`, Property: "threadEnabled"}
|
|
})
|
|
},
|
|
want: "still publishes replacement constant property",
|
|
},
|
|
{
|
|
name: "source renames constant property",
|
|
mutate: func(_, current *schemaContract, _ []interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.create_group", func(tool *toolSchema) {
|
|
tool.Parameters["thread-copy"] = parameterSchema{Type: `"boolean"`, Property: "threadEnabled"}
|
|
})
|
|
},
|
|
want: "source Schema tool still publishes replacement constant property",
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
baseline := schemaCommandMigrationContract(false)
|
|
current := schemaCommandMigrationContract(true)
|
|
migrations := schemaCommandMigrationAuthorizations()
|
|
test.mutate(&baseline, ¤t, migrations)
|
|
if _, err := normalizeSchemaCommandMigrations(baseline, current, migrations); err == nil ||
|
|
!strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("replacement constant error=%v, want %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationLifecycleAndRun(t *testing.T) {
|
|
directory := t.TempDir()
|
|
baselinePath := filepath.Join(directory, "baseline.json")
|
|
currentPath := filepath.Join(directory, "current.json")
|
|
approvedPath := filepath.Join(directory, "approved.json")
|
|
candidatePath := filepath.Join(directory, "candidate.json")
|
|
currentSnapshotPath := filepath.Join(directory, "current-snapshot.json")
|
|
baseSnapshotPath := filepath.Join(directory, "base-snapshot.json")
|
|
stableSnapshotPath := filepath.Join(directory, "stable-snapshot.json")
|
|
|
|
writeSchemaContractFile(t, baselinePath, schemaCommandMigrationContract(false))
|
|
writeRawSchemaContractFile(t, currentPath, schemaCommandMigrationContract(true))
|
|
writeCommandMigrationManifestFile(t, approvedPath, schemaCommandMigrationManifest(interfacesnapshot.CommandMigrationPending))
|
|
writeCommandMigrationManifestFile(t, candidatePath, schemaCommandMigrationManifest(interfacesnapshot.CommandMigrationConsumed))
|
|
writeInterfaceSnapshotFile(t, currentSnapshotPath, schemaCommandMigrationSnapshot(true))
|
|
writeInterfaceSnapshotFile(t, baseSnapshotPath, schemaCommandMigrationSnapshot(false))
|
|
writeInterfaceSnapshotFile(t, stableSnapshotPath, schemaCommandMigrationSnapshot(false))
|
|
|
|
args := []string{
|
|
"--check", baselinePath,
|
|
"--current", currentPath,
|
|
"--approved-command-migrations", approvedPath,
|
|
"--candidate-command-migrations", candidatePath,
|
|
"--migration-current-snapshot", currentSnapshotPath,
|
|
"--migration-base-snapshot", baseSnapshotPath,
|
|
"--migration-stable-snapshot", stableSnapshotPath,
|
|
}
|
|
var stdout, stderr strings.Builder
|
|
if code := run(args, &stdout, &stderr); code != 0 {
|
|
t.Fatalf("command migration run code=%d stderr=%s", code, stderr.String())
|
|
}
|
|
|
|
paths := []string{approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath}
|
|
for index := range paths {
|
|
invalid := append([]string(nil), paths...)
|
|
invalid[index] = filepath.Join(directory, "missing.json")
|
|
if _, err := authorizeSchemaCommandMigrations(invalid[0], invalid[1], invalid[2], invalid[3], invalid[4]); err == nil {
|
|
t.Fatalf("missing command migration input %d was accepted", index)
|
|
}
|
|
}
|
|
|
|
stderr.Reset()
|
|
if code := run([]string{"--check", baselinePath, "--current", currentPath, "--approved-command-migrations", approvedPath}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "both command manifests") {
|
|
t.Fatalf("partial command pair code=%d stderr=%q", code, stderr.String())
|
|
}
|
|
stderr.Reset()
|
|
if code := run([]string{
|
|
"--check", baselinePath,
|
|
"--current", currentPath,
|
|
"--approved-command-migrations", approvedPath,
|
|
"--candidate-command-migrations", candidatePath,
|
|
}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "all three interface snapshots") {
|
|
t.Fatalf("missing command snapshots code=%d stderr=%q", code, stderr.String())
|
|
}
|
|
stderr.Reset()
|
|
if code := run([]string{"--check", baselinePath, "--current", currentPath, "--migration-current-snapshot", currentSnapshotPath}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "require a flag or command") {
|
|
t.Fatalf("orphan snapshot code=%d stderr=%q", code, stderr.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 command migrations") {
|
|
t.Fatalf("command authorization error code=%d stderr=%q", code, stderr.String())
|
|
}
|
|
|
|
legacyCurrent := schemaCommandMigrationContract(true)
|
|
product := legacyCurrent.Products["chat"]
|
|
tool := product.Tools["chat.move"]
|
|
tool.Parameters["old-id"] = schemaCommandMigrationContract(false).Products["chat"].Tools["chat.move"].Parameters["old-id"]
|
|
product.Tools["chat.move"] = tool
|
|
legacyCurrent.Products["chat"] = product
|
|
writeRawSchemaContractFile(t, currentPath, legacyCurrent)
|
|
stderr.Reset()
|
|
if code := run(args, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "normalize approved Schema command migrations") {
|
|
t.Fatalf("command normalization error code=%d stderr=%q", code, stderr.String())
|
|
}
|
|
|
|
requiredAddition := schemaCommandMigrationContract(true)
|
|
product = requiredAddition.Products["chat"]
|
|
tool = product.Tools["chat.move"]
|
|
tool.Parameters["must"] = parameterSchema{Type: `"string"`, Property: "must", Required: true, CLIRequired: true}
|
|
product.Tools["chat.move"] = tool
|
|
requiredAddition.Products["chat"] = product
|
|
writeRawSchemaContractFile(t, currentPath, requiredAddition)
|
|
stderr.Reset()
|
|
if code := run(args, &stdout, &stderr); code != 2 ||
|
|
!strings.Contains(stderr.String(), `introduced unregistered required Schema parameter "must"`) {
|
|
t.Fatalf("unregistered required addition code=%d stderr=%q", code, stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationComposesHistoricalFlagLineage(t *testing.T) {
|
|
directory := t.TempDir()
|
|
stableContract, baseContract, currentContract := schemaCommandLineageContracts()
|
|
baselinePath := filepath.Join(directory, "stable-schema.json")
|
|
baseSchemaPath := filepath.Join(directory, "base-schema.json")
|
|
currentPath := filepath.Join(directory, "current-schema.json")
|
|
approvedFlagPath := filepath.Join(directory, "approved-flags.json")
|
|
candidateFlagPath := filepath.Join(directory, "candidate-flags.json")
|
|
approvedCommandPath := filepath.Join(directory, "approved-commands.json")
|
|
candidateCommandPath := filepath.Join(directory, "candidate-commands.json")
|
|
currentSnapshotPath := filepath.Join(directory, "current-snapshot.json")
|
|
baseSnapshotPath := filepath.Join(directory, "base-snapshot.json")
|
|
stableSnapshotPath := filepath.Join(directory, "stable-snapshot.json")
|
|
|
|
writeSchemaContractFile(t, baselinePath, stableContract)
|
|
writeSchemaContractFile(t, baseSchemaPath, baseContract)
|
|
writeRawSchemaContractFile(t, currentPath, currentContract)
|
|
writeFlagMigrationManifestFile(t, approvedFlagPath, schemaCommandLineageFlagManifest())
|
|
writeFlagMigrationManifestFile(t, candidateFlagPath, schemaCommandLineageFlagManifest())
|
|
writeCommandMigrationManifestFile(t, approvedCommandPath, schemaCommandLineageManifest(interfacesnapshot.CommandMigrationPending))
|
|
writeCommandMigrationManifestFile(t, candidateCommandPath, schemaCommandLineageManifest(interfacesnapshot.CommandMigrationConsumed))
|
|
writeInterfaceSnapshotFile(t, currentSnapshotPath, schemaCommandLineageSnapshot(true, true))
|
|
writeInterfaceSnapshotFile(t, baseSnapshotPath, schemaCommandLineageSnapshot(true, false))
|
|
writeInterfaceSnapshotFile(t, stableSnapshotPath, schemaCommandLineageSnapshot(false, false))
|
|
|
|
args := []string{
|
|
"--check", baselinePath,
|
|
"--current", currentPath,
|
|
"--migration-base-schema", baseSchemaPath,
|
|
"--approved-flag-migrations", approvedFlagPath,
|
|
"--candidate-flag-migrations", candidateFlagPath,
|
|
"--approved-command-migrations", approvedCommandPath,
|
|
"--candidate-command-migrations", candidateCommandPath,
|
|
"--migration-current-snapshot", currentSnapshotPath,
|
|
"--migration-base-snapshot", baseSnapshotPath,
|
|
"--migration-stable-snapshot", stableSnapshotPath,
|
|
}
|
|
var stdout, stderr strings.Builder
|
|
if code := run(args, &stdout, &stderr); code != 0 {
|
|
t.Fatalf("pending command lineage code=%d stderr=%s", code, stderr.String())
|
|
}
|
|
|
|
withoutBaseSchema := append([]string(nil), args[:4]...)
|
|
withoutBaseSchema = append(withoutBaseSchema, args[6:]...)
|
|
stderr.Reset()
|
|
if code := run(withoutBaseSchema, &stdout, &stderr); code != 2 ||
|
|
!strings.Contains(stderr.String(), "requires --migration-base-schema") {
|
|
t.Fatalf("missing migration base Schema code=%d stderr=%q", code, stderr.String())
|
|
}
|
|
stderr.Reset()
|
|
if code := run([]string{
|
|
"--check", baselinePath,
|
|
"--current", currentPath,
|
|
"--migration-base-schema", baseSchemaPath,
|
|
}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "requires both flag and command") {
|
|
t.Fatalf("orphan migration base Schema code=%d stderr=%q", code, stderr.String())
|
|
}
|
|
missingBaseSchema := append([]string(nil), args...)
|
|
missingBaseSchema[5] = filepath.Join(directory, "missing-base-schema.json")
|
|
stderr.Reset()
|
|
if code := run(missingBaseSchema, &stdout, &stderr); code != 2 ||
|
|
!strings.Contains(stderr.String(), "read migration merge-base Schema contract") {
|
|
t.Fatalf("unreadable migration base Schema code=%d stderr=%q", code, stderr.String())
|
|
}
|
|
|
|
// After the command move is merged, the merge-base Schema is already at the
|
|
// final name. The two consumed receipts must keep the stable lineage durable
|
|
// until the stable release itself reaches the after state.
|
|
writeSchemaContractFile(t, baseSchemaPath, currentContract)
|
|
writeCommandMigrationManifestFile(t, approvedCommandPath, schemaCommandLineageManifest(interfacesnapshot.CommandMigrationConsumed))
|
|
writeInterfaceSnapshotFile(t, baseSnapshotPath, schemaCommandLineageSnapshot(true, true))
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
if code := run(args, &stdout, &stderr); code != 0 {
|
|
t.Fatalf("consumed command lineage code=%d stderr=%s", code, stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationLineageFailsClosed(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
commandState string
|
|
mutate func(*schemaContract, *schemaContract, *schemaContract, *[]interfacesnapshot.FlagMigration, *[]interfacesnapshot.CommandMigration)
|
|
want string
|
|
}{
|
|
{
|
|
name: "pending merge-base missing intermediate",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
delete(tool.Parameters, "conversation-id")
|
|
})
|
|
},
|
|
want: "merge-base Schema lacks intermediate parameter",
|
|
},
|
|
{
|
|
name: "merge-base missing source tool",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
product := base.Products["chat"]
|
|
delete(product.Tools, "chat.list_topic_replies")
|
|
base.Products["chat"] = product
|
|
},
|
|
want: "merge-base Schema lacks source tool",
|
|
},
|
|
{
|
|
name: "pending merge-base parameter drift",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(base, "chat.list_topic_replies", "conversation-id", func(parameter *parameterSchema) {
|
|
parameter.Property = "differentProperty"
|
|
})
|
|
},
|
|
want: "changed a non-migration field",
|
|
},
|
|
{
|
|
name: "pending merge-base wrong path",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.PrimaryCLIPath = "chat unrelated"
|
|
})
|
|
},
|
|
want: "merge-base Schema source tool has primary_cli_path",
|
|
},
|
|
{
|
|
name: "pending merge-base already publishes final",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["open-topic-id"] = tool.Parameters["conversation-id"]
|
|
})
|
|
},
|
|
want: "already publishes final parameter",
|
|
},
|
|
{
|
|
name: "pending flag receipt",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, _, _ *schemaContract, flags *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
(*flags)[0].State = interfacesnapshot.FlagMigrationPending
|
|
},
|
|
want: "requires a consumed flag migration receipt",
|
|
},
|
|
{
|
|
name: "flag receipt belongs to another command",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, _, _ *schemaContract, flags *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
(*flags)[0].Command = "dws chat message other"
|
|
},
|
|
want: `historical Schema tool lacks parameter "conversation-id"`,
|
|
},
|
|
{
|
|
name: "current retains predecessor",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(stable, _, current *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["group"] = stable.Products["chat"].Tools["chat.list_topic_replies"].Parameters["group"]
|
|
})
|
|
},
|
|
want: "current Schema still publishes predecessor parameter",
|
|
},
|
|
{
|
|
name: "pending current constraints retain predecessor",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, _, current *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Constraints = `{"require_together":[["group","open-conv-thread-id","open-topic-id"]]}`
|
|
})
|
|
},
|
|
want: "current Schema constraints still reference predecessor parameter",
|
|
},
|
|
{
|
|
name: "consumed current constraints retain predecessor",
|
|
commandState: interfacesnapshot.CommandMigrationConsumed,
|
|
mutate: func(_, _, current *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Constraints = `{"require_together":[["group","open-conv-thread-id","open-topic-id"]]}`
|
|
})
|
|
},
|
|
want: "current Schema constraints still reference predecessor parameter",
|
|
},
|
|
{
|
|
name: "current retains intermediate",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, base, current *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["conversation-id"] = base.Products["chat"].Tools["chat.list_topic_replies"].Parameters["conversation-id"]
|
|
})
|
|
},
|
|
want: `still publishes legacy Schema parameter "conversation-id"`,
|
|
},
|
|
{
|
|
name: "consumed merge-base final drift",
|
|
commandState: interfacesnapshot.CommandMigrationConsumed,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationParameter(base, "chat.list_topic_replies", "open-topic-id", func(parameter *parameterSchema) {
|
|
parameter.Required = false
|
|
})
|
|
},
|
|
want: "changed a non-name field",
|
|
},
|
|
{
|
|
name: "consumed merge-base wrong path",
|
|
commandState: interfacesnapshot.CommandMigrationConsumed,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.PrimaryCLIPath = "chat unrelated"
|
|
})
|
|
},
|
|
want: "consumed command migration",
|
|
},
|
|
{
|
|
name: "consumed merge-base missing final",
|
|
commandState: interfacesnapshot.CommandMigrationConsumed,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
delete(tool.Parameters, "open-topic-id")
|
|
})
|
|
},
|
|
want: "merge-base Schema lacks final parameter",
|
|
},
|
|
{
|
|
name: "consumed merge-base retains intermediate",
|
|
commandState: interfacesnapshot.CommandMigrationConsumed,
|
|
mutate: func(stable, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["conversation-id"] = stable.Products["chat"].Tools["chat.list_topic_replies"].Parameters["group"]
|
|
})
|
|
},
|
|
want: "still publishes intermediate parameter",
|
|
},
|
|
{
|
|
name: "pending constraints drift",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Constraints = `{"require_one_of":[["conversation-id"]]}`
|
|
})
|
|
},
|
|
want: "changed merge-base Schema constraints",
|
|
},
|
|
{
|
|
name: "pending current keeps intermediate constraints",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, base, current *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(current, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Constraints = base.Products["chat"].Tools["chat.list_topic_replies"].Constraints
|
|
})
|
|
},
|
|
want: "still reference legacy Schema constraint parameter",
|
|
},
|
|
{
|
|
name: "merge-base retains predecessor",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(stable, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["group"] = stable.Products["chat"].Tools["chat.list_topic_replies"].Parameters["group"]
|
|
})
|
|
},
|
|
want: "merge-base Schema still publishes predecessor parameter",
|
|
},
|
|
{
|
|
name: "historical constraints malformed",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(stable, _, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(stable, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Constraints = "not-json"
|
|
})
|
|
},
|
|
want: "historical Schema constraints are not canonicalizable",
|
|
},
|
|
{
|
|
name: "merge-base constraints malformed",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Constraints = "not-json"
|
|
})
|
|
},
|
|
want: "merge-base Schema constraints are not canonicalizable",
|
|
},
|
|
{
|
|
name: "consumed constraints drift",
|
|
commandState: interfacesnapshot.CommandMigrationConsumed,
|
|
mutate: func(_, base, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Constraints = `{"require_one_of":[["open-topic-id"]]}`
|
|
})
|
|
},
|
|
want: "changed merge-base Schema constraints",
|
|
},
|
|
{
|
|
name: "target collision",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(stable, _, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
mutateSchemaCommandMigrationTool(stable, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["open-topic-id"] = tool.Parameters["group"]
|
|
})
|
|
},
|
|
want: "target \"open-topic-id\" already exists",
|
|
},
|
|
{
|
|
name: "lineage cycle",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, _, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, commands *[]interfacesnapshot.CommandMigration) {
|
|
(*commands)[0].Schema.Parameters[0].To = "group"
|
|
},
|
|
want: "lineage cycle",
|
|
},
|
|
{
|
|
name: "unsupported command receipt state",
|
|
commandState: "unknown",
|
|
mutate: func(_, _, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
},
|
|
want: "unsupported lineage state",
|
|
},
|
|
{
|
|
name: "duplicate historical path",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(stable, _, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, _ *[]interfacesnapshot.CommandMigration) {
|
|
product := stable.Products["chat"]
|
|
product.Tools["chat.duplicate"] = product.Tools["chat.list_topic_replies"]
|
|
stable.Products["chat"] = product
|
|
},
|
|
want: "matches 2 historical Schema tools",
|
|
},
|
|
{
|
|
name: "source tool fork",
|
|
commandState: interfacesnapshot.CommandMigrationPending,
|
|
mutate: func(_, _, _ *schemaContract, _ *[]interfacesnapshot.FlagMigration, commands *[]interfacesnapshot.CommandMigration) {
|
|
fork := (*commands)[0]
|
|
fork.Legacy.Command = "dws chat message fork"
|
|
fork.Replacement.Command = "dws chat topic fork"
|
|
*commands = append(*commands, fork)
|
|
},
|
|
want: "fork Schema source tool",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
stable, baseBefore, current := schemaCommandLineageContracts()
|
|
base := baseBefore
|
|
if test.commandState == interfacesnapshot.CommandMigrationConsumed {
|
|
base = cloneContract(current)
|
|
}
|
|
flags := append([]interfacesnapshot.FlagMigration(nil), schemaCommandLineageFlagManifest().Migrations...)
|
|
commands := append([]interfacesnapshot.CommandMigration(nil), schemaCommandLineageManifest(test.commandState).Migrations...)
|
|
test.mutate(&stable, &base, ¤t, &flags, &commands)
|
|
if _, err := normalizeSchemaCommandMigrationLineage(stable, base, current, flags, commands); err == nil ||
|
|
!strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("lineage error=%v, want %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationLineagePreservesOrdinaryChecks(t *testing.T) {
|
|
stable, base, current := schemaCommandLineageContracts()
|
|
mutateSchemaCommandMigrationTool(¤t, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Positionals = []positionalSchema{{Name: "open-topic-id", Index: 0, Type: "string", Required: true}}
|
|
})
|
|
normalized, err := normalizeSchemaCommandMigrationLineage(
|
|
stable,
|
|
base,
|
|
current,
|
|
schemaCommandLineageFlagManifest().Migrations,
|
|
schemaCommandLineageManifest(interfacesnapshot.CommandMigrationPending).Migrations,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if failures := strings.Join(checkCompatibility(normalized, current), "\n"); !strings.Contains(failures, "changed positionals") {
|
|
t.Fatalf("lineage hid positional drift: %s", failures)
|
|
}
|
|
|
|
// Multiple historical names may converge only when every predecessor carries
|
|
// the exact same contract and every receipt is already consumed.
|
|
stable, base, current = schemaCommandLineageContracts()
|
|
mutateSchemaCommandMigrationTool(&stable, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["id"] = tool.Parameters["group"]
|
|
})
|
|
flags := schemaCommandLineageFlagManifest().Migrations
|
|
second := flags[0]
|
|
second.Legacy.Name = "id"
|
|
flags = append(flags, second)
|
|
normalized, err = normalizeSchemaCommandMigrationLineage(
|
|
stable,
|
|
base,
|
|
current,
|
|
flags,
|
|
schemaCommandLineageManifest(interfacesnapshot.CommandMigrationPending).Migrations,
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("equivalent predecessor aliases should pass: %v", err)
|
|
}
|
|
if failures := checkCompatibility(normalized, current); len(failures) != 0 {
|
|
t.Fatalf("equivalent predecessor aliases remained incompatible: %v", failures)
|
|
}
|
|
|
|
mutateSchemaCommandMigrationParameter(&stable, "chat.list_topic_replies", "id", func(parameter *parameterSchema) {
|
|
parameter.Property = "differentProperty"
|
|
})
|
|
if _, err := normalizeSchemaCommandMigrationLineage(
|
|
stable,
|
|
base,
|
|
current,
|
|
flags,
|
|
schemaCommandLineageManifest(interfacesnapshot.CommandMigrationPending).Migrations,
|
|
); err == nil || !strings.Contains(err.Error(), "changed a non-migration field") {
|
|
t.Fatalf("drifted predecessor alias error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCrossPlatformCoverageSchemaCommandMigrationLineageDefensiveEdges(t *testing.T) {
|
|
stable, base, current := schemaCommandLineageContracts()
|
|
flags := schemaCommandLineageFlagManifest().Migrations
|
|
commands := schemaCommandLineageManifest(interfacesnapshot.CommandMigrationPending).Migrations
|
|
|
|
flagExtraction := schemaCommandMigrationAuthorizations()[1]
|
|
if _, err := stageSchemaCommandMigrationPredecessors(stable, base, current, flags, []interfacesnapshot.CommandMigration{flagExtraction}); err != nil {
|
|
t.Fatalf("non-move command migration should be ignored: %v", err)
|
|
}
|
|
|
|
missingTool := cloneContract(stable)
|
|
product := missingTool.Products["chat"]
|
|
delete(product.Tools, "chat.list_topic_replies")
|
|
missingTool.Products["chat"] = product
|
|
if _, err := stageSchemaCommandMigrationPredecessors(missingTool, base, current, flags, commands); err != nil {
|
|
t.Fatalf("missing historical source should remain for the ordinary checker: %v", err)
|
|
}
|
|
if _, err := stageSchemaCommandMigrationPredecessors(current, current, current, flags, schemaCommandLineageManifest(interfacesnapshot.CommandMigrationConsumed).Migrations); err != nil {
|
|
t.Fatalf("already-after historical source should be a no-op: %v", err)
|
|
}
|
|
wrongPath := cloneContract(stable)
|
|
mutateSchemaCommandMigrationTool(&wrongPath, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.PrimaryCLIPath = "chat unrelated"
|
|
})
|
|
if _, err := stageSchemaCommandMigrationPredecessors(wrongPath, base, current, flags, commands); err != nil {
|
|
t.Fatalf("wrong historical path should remain for the command normalizer: %v", err)
|
|
}
|
|
|
|
bothNames := cloneContract(stable)
|
|
mutateSchemaCommandMigrationTool(&bothNames, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["conversation-id"] = tool.Parameters["group"]
|
|
})
|
|
if _, err := stageSchemaCommandMigrationPredecessors(bothNames, base, current, flags, commands); err == nil ||
|
|
!strings.Contains(err.Error(), "publishes both predecessor") {
|
|
t.Fatalf("ambiguous predecessor error=%v", err)
|
|
}
|
|
|
|
duplicatePath := cloneContract(stable)
|
|
product = duplicatePath.Products["chat"]
|
|
product.Tools["chat.duplicate"] = product.Tools["chat.list_topic_replies"]
|
|
duplicatePath.Products["chat"] = product
|
|
if _, err := stageSchemaCommandMigrationPredecessors(duplicatePath, base, current, flags, commands); err == nil ||
|
|
!strings.Contains(err.Error(), "requires one exact historical Schema tool") {
|
|
t.Fatalf("duplicate primary path error=%v", err)
|
|
}
|
|
|
|
forkStable, forkBase, forkCurrent := schemaCommandLineageContracts()
|
|
mutateSchemaCommandMigrationTool(&forkBase, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["other-mid"] = tool.Parameters["conversation-id"]
|
|
})
|
|
mutateSchemaCommandMigrationTool(&forkCurrent, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["other-final"] = tool.Parameters["open-topic-id"]
|
|
})
|
|
forkFlags := append([]interfacesnapshot.FlagMigration(nil), flags...)
|
|
forkFlag := flags[0]
|
|
forkFlag.Canonical.Name = "other-mid"
|
|
forkFlags = append(forkFlags, forkFlag)
|
|
forkCommands := schemaCommandLineageManifest(interfacesnapshot.CommandMigrationPending).Migrations
|
|
forkCommands[0].Schema.Parameters = append(forkCommands[0].Schema.Parameters,
|
|
interfacesnapshot.CommandParameterMigration{From: "other-mid", To: "other-final"})
|
|
if _, err := stageSchemaCommandMigrationPredecessors(forkStable, forkBase, forkCurrent, forkFlags, forkCommands); err == nil ||
|
|
!strings.Contains(err.Error(), "forks Schema predecessor") {
|
|
t.Fatalf("forked predecessor error=%v", err)
|
|
}
|
|
|
|
cycleStable, cycleBase, _ := schemaCommandLineageContracts()
|
|
mutateSchemaCommandMigrationTool(&cycleStable, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["id"] = tool.Parameters["group"]
|
|
})
|
|
mutateSchemaCommandMigrationTool(&cycleBase, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
tool.Parameters["other-mid"] = tool.Parameters["conversation-id"]
|
|
})
|
|
cycleFlags := append([]interfacesnapshot.FlagMigration(nil), flags...)
|
|
cycleFlag := flags[0]
|
|
cycleFlag.Legacy.Name = "id"
|
|
cycleFlag.Canonical.Name = "other-mid"
|
|
cycleFlags = append(cycleFlags, cycleFlag)
|
|
cycleCommands := schemaCommandLineageManifest(interfacesnapshot.CommandMigrationPending).Migrations
|
|
cycleCommands[0].Schema.Parameters[0].To = "id"
|
|
cycleCommands[0].Schema.Parameters = append(cycleCommands[0].Schema.Parameters,
|
|
interfacesnapshot.CommandParameterMigration{From: "other-mid", To: "other-final"})
|
|
missingCurrent := schemaContract{Version: schemaContractVersion, Products: map[string]productSchema{}}
|
|
if _, err := stageSchemaCommandMigrationPredecessors(cycleStable, cycleBase, missingCurrent, cycleFlags, cycleCommands); err == nil ||
|
|
!strings.Contains(err.Error(), "lineage cycle") {
|
|
t.Fatalf("cross-lineage cycle error=%v", err)
|
|
}
|
|
}
|
|
|
|
func schemaCommandMigrationContract(after bool) schemaContract {
|
|
id := parameterSchema{Type: `"string"`, Property: "resourceId", Required: true, CLIRequired: true}
|
|
keep := parameterSchema{Type: `"string"`, Property: "keep"}
|
|
name := parameterSchema{Type: `"string"`, Property: "name"}
|
|
groupType := parameterSchema{Type: `"string"`, Property: "type"}
|
|
users := parameterSchema{Type: `"array"`, Property: "userIds"}
|
|
thread := parameterSchema{
|
|
Type: `"boolean"`,
|
|
Property: "threadEnabled",
|
|
InterfaceType: "boolean",
|
|
Default: "false",
|
|
InterfaceDefault: "false",
|
|
Enum: []string{"false", "true"},
|
|
}
|
|
group := toolSchema{
|
|
PrimaryCLIPath: "chat group create",
|
|
InterfaceMode: "mcp",
|
|
InterfaceRef: `{"product_id":"im","rpc_name":"create_group"}`,
|
|
Availability: "available",
|
|
Parameters: map[string]parameterSchema{
|
|
"name": name,
|
|
"thread": thread,
|
|
"type": groupType,
|
|
"users": users,
|
|
},
|
|
Constraints: `{"require_together":[["name","type"]]}`,
|
|
Positionals: []positionalSchema{{Name: "name", Index: 0, Type: "string", Required: true}},
|
|
Effect: "write",
|
|
Risk: "medium",
|
|
Confirmation: "not_required",
|
|
Idempotency: "unknown",
|
|
}
|
|
move := toolSchema{
|
|
PrimaryCLIPath: "chat message old",
|
|
InterfaceMode: "mcp",
|
|
InterfaceRef: `{"product_id":"chat","rpc_name":"move"}`,
|
|
Availability: "available",
|
|
Parameters: map[string]parameterSchema{"old-id": id, "keep": keep},
|
|
Constraints: `{"require_together":[["keep","old-id"]]}`,
|
|
Effect: "read",
|
|
Risk: "low",
|
|
Confirmation: "not_required",
|
|
Idempotency: "idempotent",
|
|
}
|
|
tools := map[string]toolSchema{"chat.create_group": group, "chat.move": move}
|
|
if after {
|
|
delete(group.Parameters, "thread")
|
|
tools["chat.create_group"] = group
|
|
replacement := group
|
|
replacement.Parameters = make(map[string]parameterSchema, len(group.Parameters))
|
|
for name, parameter := range group.Parameters {
|
|
replacement.Parameters[name] = parameter
|
|
}
|
|
replacement.Positionals = append([]positionalSchema(nil), group.Positionals...)
|
|
replacement.PrimaryCLIPath = "chat topic create"
|
|
tools["chat.create_topic"] = replacement
|
|
delete(move.Parameters, "old-id")
|
|
move.Parameters["new-id"] = id
|
|
move.PrimaryCLIPath = "chat topic new"
|
|
move.Constraints = `{"require_together":[["keep","new-id"]]}`
|
|
tools["chat.move"] = move
|
|
}
|
|
return schemaContract{Version: schemaContractVersion, Products: map[string]productSchema{"chat": {Tools: tools}}}
|
|
}
|
|
|
|
func schemaCommandLineageContracts() (schemaContract, schemaContract, schemaContract) {
|
|
conversation := parameterSchema{
|
|
Type: `"string"`,
|
|
Property: "openconversationId",
|
|
InterfaceType: "string",
|
|
Required: true,
|
|
CLIRequired: true,
|
|
}
|
|
topic := parameterSchema{
|
|
Type: `"string"`,
|
|
Property: "openConversationThreadId",
|
|
InterfaceType: "string",
|
|
Required: true,
|
|
CLIRequired: true,
|
|
}
|
|
tool := toolSchema{
|
|
PrimaryCLIPath: "chat message list-topic-replies",
|
|
InterfaceMode: "mcp",
|
|
InterfaceRef: `{"product_id":"im","rpc_name":"list_topic_replies"}`,
|
|
Availability: "available",
|
|
Parameters: map[string]parameterSchema{
|
|
"group": conversation,
|
|
"topic-id": topic,
|
|
},
|
|
Constraints: `{"require_together":[["group","topic-id"]]}`,
|
|
Effect: "read",
|
|
Risk: "low",
|
|
Confirmation: "not_required",
|
|
Idempotency: "idempotent",
|
|
}
|
|
stable := schemaContract{Version: schemaContractVersion, Products: map[string]productSchema{
|
|
"chat": {Tools: map[string]toolSchema{"chat.list_topic_replies": tool}},
|
|
}}
|
|
|
|
base := cloneContract(stable)
|
|
mutateSchemaCommandMigrationTool(&base, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
delete(tool.Parameters, "group")
|
|
tool.Parameters["conversation-id"] = conversation
|
|
tool.Constraints = `{"require_together":[["conversation-id","topic-id"]]}`
|
|
})
|
|
|
|
current := cloneContract(base)
|
|
mutateSchemaCommandMigrationTool(¤t, "chat.list_topic_replies", func(tool *toolSchema) {
|
|
delete(tool.Parameters, "conversation-id")
|
|
delete(tool.Parameters, "topic-id")
|
|
tool.Parameters["open-topic-id"] = conversation
|
|
tool.Parameters["open-conv-thread-id"] = topic
|
|
tool.PrimaryCLIPath = "chat topic list-replies"
|
|
tool.Constraints = `{"require_together":[["open-conv-thread-id","open-topic-id"]]}`
|
|
})
|
|
return stable, base, current
|
|
}
|
|
|
|
func schemaCommandLineageFlagManifest() interfacesnapshot.FlagMigrationManifest {
|
|
beforeCanonical := interfacesnapshot.FlagMigrationState{Present: true, Type: "string", Hidden: true, Scope: "local"}
|
|
afterCanonical := interfacesnapshot.FlagMigrationState{Present: true, Type: "string", Required: true, Scope: "local"}
|
|
return interfacesnapshot.FlagMigrationManifest{
|
|
Version: interfacesnapshot.FlagMigrationManifestVersion,
|
|
Migrations: []interfacesnapshot.FlagMigration{{
|
|
Command: "dws chat message list-topic-replies",
|
|
Legacy: interfacesnapshot.FlagMigrationSide{
|
|
Name: "group",
|
|
Before: interfacesnapshot.FlagMigrationState{Present: true, Type: "string", Required: true, Scope: "local"},
|
|
After: interfacesnapshot.FlagMigrationState{Present: true, Type: "string", Hidden: true, Scope: "local", AliasOf: "conversation-id"},
|
|
},
|
|
Canonical: interfacesnapshot.FlagMigrationSide{
|
|
Name: "conversation-id",
|
|
Before: beforeCanonical,
|
|
After: afterCanonical,
|
|
},
|
|
State: interfacesnapshot.FlagMigrationConsumed,
|
|
Reason: "preserve the reviewed group to conversation-id lineage",
|
|
}},
|
|
}
|
|
}
|
|
|
|
func schemaCommandLineageManifest(state string) interfacesnapshot.CommandMigrationManifest {
|
|
return interfacesnapshot.CommandMigrationManifest{
|
|
Version: interfacesnapshot.CommandMigrationManifestVersion,
|
|
Migrations: []interfacesnapshot.CommandMigration{{
|
|
Kind: interfacesnapshot.CommandMigrationMove,
|
|
Legacy: interfacesnapshot.CommandMigrationSide{
|
|
Command: "dws chat message list-topic-replies",
|
|
Before: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true},
|
|
After: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true, Hidden: true},
|
|
},
|
|
Replacement: interfacesnapshot.CommandMigrationSide{
|
|
Command: "dws chat topic list-replies",
|
|
Before: interfacesnapshot.CommandMigrationState{},
|
|
After: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true},
|
|
},
|
|
Schema: interfacesnapshot.CommandMigrationSchema{
|
|
ProductID: "chat",
|
|
SourceToolID: "chat.list_topic_replies",
|
|
ReplacementToolID: "chat.list_topic_replies",
|
|
Parameters: []interfacesnapshot.CommandParameterMigration{
|
|
{From: "conversation-id", To: "open-topic-id"},
|
|
{From: "topic-id", To: "open-conv-thread-id"},
|
|
},
|
|
},
|
|
State: state,
|
|
Reason: "move topic reply listing while retaining the legacy command",
|
|
}},
|
|
}
|
|
}
|
|
|
|
func schemaCommandLineageSnapshot(flagAfter, commandAfter bool) interfacesnapshot.Snapshot {
|
|
legacyFlags := []interfacesnapshot.Flag{
|
|
{Name: "conversation-id", Type: "string", Hidden: true},
|
|
{Name: "group", Type: "string", Required: true},
|
|
{Name: "topic-id", Type: "string", Required: true},
|
|
}
|
|
if flagAfter {
|
|
legacyFlags[0].Hidden = false
|
|
legacyFlags[0].Required = true
|
|
legacyFlags[1].Required = false
|
|
legacyFlags[1].Hidden = true
|
|
legacyFlags[1].AliasOf = "conversation-id"
|
|
}
|
|
commands := []interfacesnapshot.Command{
|
|
{Path: "dws", Runnable: true, Aliases: []string{}, LocalFlags: []interfacesnapshot.Flag{}, InheritedFlags: []interfacesnapshot.Flag{}},
|
|
{
|
|
Path: "dws chat message list-topic-replies",
|
|
Runnable: true,
|
|
Aliases: []string{},
|
|
LocalFlags: legacyFlags,
|
|
InheritedFlags: []interfacesnapshot.Flag{},
|
|
},
|
|
}
|
|
if commandAfter {
|
|
commands[1].Hidden = true
|
|
commands = append(commands, interfacesnapshot.Command{
|
|
Path: "dws chat topic list-replies",
|
|
Runnable: true,
|
|
Aliases: []string{},
|
|
LocalFlags: []interfacesnapshot.Flag{},
|
|
InheritedFlags: []interfacesnapshot.Flag{},
|
|
})
|
|
}
|
|
return interfacesnapshot.Snapshot{
|
|
SchemaVersion: interfacesnapshot.SchemaVersion,
|
|
Rules: interfacesnapshot.Rules{
|
|
ExcludedCommandSubtrees: []string{"dws __complete", "dws __completeNoDesc", "dws completion", "dws help"},
|
|
ExcludedFlags: []string{"help"},
|
|
},
|
|
Commands: commands,
|
|
}
|
|
}
|
|
|
|
func schemaCommandMigrationAuthorizations() []interfacesnapshot.CommandMigration {
|
|
return []interfacesnapshot.CommandMigration{
|
|
{
|
|
Kind: interfacesnapshot.CommandMigrationMove,
|
|
Legacy: interfacesnapshot.CommandMigrationSide{
|
|
Command: "dws chat message old",
|
|
Before: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true},
|
|
After: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true, Hidden: true},
|
|
},
|
|
Replacement: interfacesnapshot.CommandMigrationSide{
|
|
Command: "dws chat topic new",
|
|
After: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true},
|
|
},
|
|
Schema: interfacesnapshot.CommandMigrationSchema{
|
|
ProductID: "chat",
|
|
SourceToolID: "chat.move",
|
|
ReplacementToolID: "chat.move",
|
|
Parameters: []interfacesnapshot.CommandParameterMigration{{From: "old-id", To: "new-id"}},
|
|
},
|
|
},
|
|
{
|
|
Kind: interfacesnapshot.CommandMigrationFlagExtraction,
|
|
Legacy: interfacesnapshot.CommandMigrationSide{
|
|
Command: "dws chat group create",
|
|
Before: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true},
|
|
After: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true},
|
|
},
|
|
Replacement: interfacesnapshot.CommandMigrationSide{
|
|
Command: "dws chat topic create",
|
|
After: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true},
|
|
},
|
|
LegacyFlag: interfacesnapshot.CommandMigrationFlag{
|
|
Name: "thread",
|
|
Before: interfacesnapshot.FlagMigrationState{Present: true, Type: "bool", NoOpt: "true", Scope: "local"},
|
|
After: interfacesnapshot.FlagMigrationState{Present: true, Type: "bool", Hidden: true, NoOpt: "true", Scope: "local"},
|
|
},
|
|
Schema: interfacesnapshot.CommandMigrationSchema{
|
|
ProductID: "chat",
|
|
SourceToolID: "chat.create_group",
|
|
ReplacementToolID: "chat.create_topic",
|
|
Parameters: []interfacesnapshot.CommandParameterMigration{
|
|
{From: "name", To: "name"},
|
|
{From: "type", To: "type"},
|
|
{From: "users", To: "users"},
|
|
{
|
|
From: "thread",
|
|
ReplacementConstant: &interfacesnapshot.CommandReplacementConstant{
|
|
Property: "threadEnabled",
|
|
Value: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func schemaCommandMigrationManifest(state string) interfacesnapshot.CommandMigrationManifest {
|
|
migrations := schemaCommandMigrationAuthorizations()
|
|
for index := range migrations {
|
|
migrations[index].State = state
|
|
migrations[index].Reason = "Reviewed Schema command migration."
|
|
}
|
|
return interfacesnapshot.CommandMigrationManifest{
|
|
Version: interfacesnapshot.CommandMigrationManifestVersion,
|
|
Migrations: migrations,
|
|
}
|
|
}
|
|
|
|
func schemaCommandMigrationSnapshot(after bool) interfacesnapshot.Snapshot {
|
|
commands := []interfacesnapshot.Command{
|
|
{Path: "dws", Runnable: true, Aliases: []string{}, LocalFlags: []interfacesnapshot.Flag{}, InheritedFlags: []interfacesnapshot.Flag{}},
|
|
{
|
|
Path: "dws chat group create",
|
|
Runnable: true,
|
|
Aliases: []string{},
|
|
LocalFlags: []interfacesnapshot.Flag{{
|
|
Name: "thread", Type: "bool", NoOpt: "true",
|
|
}},
|
|
InheritedFlags: []interfacesnapshot.Flag{},
|
|
},
|
|
{Path: "dws chat message old", Runnable: true, Aliases: []string{}, LocalFlags: []interfacesnapshot.Flag{}, InheritedFlags: []interfacesnapshot.Flag{}},
|
|
}
|
|
if after {
|
|
commands[1].LocalFlags[0].Hidden = true
|
|
commands[2].Hidden = true
|
|
commands = append(commands,
|
|
interfacesnapshot.Command{
|
|
Path: "dws chat topic create",
|
|
Runnable: true,
|
|
Aliases: []string{},
|
|
LocalFlags: []interfacesnapshot.Flag{},
|
|
InheritedFlags: []interfacesnapshot.Flag{},
|
|
BoolConstParams: map[string]bool{"threadEnabled": true},
|
|
},
|
|
interfacesnapshot.Command{Path: "dws chat topic new", Runnable: true, Aliases: []string{}, LocalFlags: []interfacesnapshot.Flag{}, InheritedFlags: []interfacesnapshot.Flag{}},
|
|
)
|
|
}
|
|
return interfacesnapshot.Snapshot{
|
|
SchemaVersion: interfacesnapshot.SchemaVersion,
|
|
Rules: interfacesnapshot.Rules{
|
|
ExcludedCommandSubtrees: []string{"dws __complete", "dws __completeNoDesc", "dws completion", "dws help"},
|
|
ExcludedFlags: []string{"help"},
|
|
},
|
|
Commands: commands,
|
|
}
|
|
}
|
|
|
|
func mutateSchemaCommandMigrationTool(contract *schemaContract, toolID string, mutate func(*toolSchema)) {
|
|
product := contract.Products["chat"]
|
|
tool := product.Tools[toolID]
|
|
mutate(&tool)
|
|
product.Tools[toolID] = tool
|
|
contract.Products["chat"] = product
|
|
}
|
|
|
|
func mutateSchemaCommandMigrationParameter(
|
|
contract *schemaContract,
|
|
toolID string,
|
|
parameterName string,
|
|
mutate func(*parameterSchema),
|
|
) {
|
|
mutateSchemaCommandMigrationTool(contract, toolID, func(tool *toolSchema) {
|
|
parameter := tool.Parameters[parameterName]
|
|
mutate(¶meter)
|
|
tool.Parameters[parameterName] = parameter
|
|
})
|
|
}
|
|
|
|
func writeCommandMigrationManifestFile(t *testing.T, path string, manifest interfacesnapshot.CommandMigrationManifest) {
|
|
t.Helper()
|
|
file, err := os.Create(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.NewEncoder(file).Encode(manifest); err != nil {
|
|
_ = file.Close()
|
|
t.Fatal(err)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|