Merge remote-tracking branch 'upstream/main' into codex/schema-compat-float-image-one-of

This commit is contained in:
xiatian
2026-08-24 18:24:13 +08:00
4 changed files with 273 additions and 19 deletions
@@ -21,6 +21,7 @@ const (
CommandMigrationMove = "command_move"
CommandMigrationFlagExtraction = "flag_extraction"
CommandMigrationAvailability = "schema_availability_hardening"
)
// CommandMigrationManifest governs compatibility-preserving surface moves that
@@ -64,6 +65,12 @@ type CommandMigrationSchema struct {
SourceToolID string `json:"source_tool_id"`
ReplacementToolID string `json:"replacement_tool_id"`
Parameters []CommandParameterMigration `json:"parameters"`
Availability *CommandAvailabilityChange `json:"availability,omitempty"`
}
type CommandAvailabilityChange struct {
Before string `json:"before"`
After string `json:"after"`
}
type CommandParameterMigration struct {
@@ -126,14 +133,23 @@ func (m CommandMigrationManifest) Validate() error {
}
func (m CommandMigration) validate() error {
if m.Kind != CommandMigrationMove && m.Kind != CommandMigrationFlagExtraction {
if m.Kind != CommandMigrationMove && m.Kind != CommandMigrationFlagExtraction && m.Kind != CommandMigrationAvailability {
return fmt.Errorf("invalid kind %q", m.Kind)
}
if !isExactCommandPath(m.Legacy.Command) || !isExactCommandPath(m.Replacement.Command) {
return fmt.Errorf("legacy and replacement must be exact command paths rooted at dws")
}
if m.Legacy.Command == m.Replacement.Command {
return fmt.Errorf("legacy and replacement command paths must differ")
if m.Kind == CommandMigrationAvailability {
if !isExactCommandPath(m.Legacy.Command) {
return fmt.Errorf("legacy must be an exact command path rooted at dws")
}
if m.Replacement != (CommandMigrationSide{}) {
return fmt.Errorf("schema_availability_hardening must not declare replacement")
}
} else {
if !isExactCommandPath(m.Legacy.Command) || !isExactCommandPath(m.Replacement.Command) {
return fmt.Errorf("legacy and replacement must be exact command paths rooted at dws")
}
if m.Legacy.Command == m.Replacement.Command {
return fmt.Errorf("legacy and replacement command paths must differ")
}
}
if strings.TrimSpace(m.Reason) == "" || m.Reason != strings.TrimSpace(m.Reason) {
return fmt.Errorf("migration must include a non-empty trimmed reason")
@@ -141,12 +157,15 @@ func (m CommandMigration) validate() error {
if m.State != CommandMigrationPending && m.State != CommandMigrationConsumed {
return fmt.Errorf("invalid state %q", m.State)
}
for label, state := range map[string]CommandMigrationState{
"legacy before": m.Legacy.Before,
"legacy after": m.Legacy.After,
"replacement before": m.Replacement.Before,
"replacement after": m.Replacement.After,
} {
states := map[string]CommandMigrationState{
"legacy before": m.Legacy.Before,
"legacy after": m.Legacy.After,
}
if m.Kind != CommandMigrationAvailability {
states["replacement before"] = m.Replacement.Before
states["replacement after"] = m.Replacement.After
}
for label, state := range states {
if err := state.validate(label); err != nil {
return err
}
@@ -155,7 +174,8 @@ func (m CommandMigration) validate() error {
!m.Legacy.After.Present || !m.Legacy.After.Runnable {
return fmt.Errorf("legacy command must remain runnable and start visible")
}
if m.Replacement.Before.Present || !m.Replacement.After.Present || !m.Replacement.After.Runnable || m.Replacement.After.Hidden {
if m.Kind != CommandMigrationAvailability &&
(m.Replacement.Before.Present || !m.Replacement.After.Present || !m.Replacement.After.Runnable || m.Replacement.After.Hidden) {
return fmt.Errorf("replacement command must migrate exactly from absent to visible runnable")
}
if err := m.Schema.validate(m.Kind); err != nil {
@@ -214,6 +234,13 @@ func (m CommandMigration) validate() error {
if m.LegacyFlag.Before.NoOpt != wantNoOpt || m.LegacyFlag.After.NoOpt != wantNoOpt {
return fmt.Errorf("flag_extraction legacy_flag no_opt must equal replacement constant %q", wantNoOpt)
}
case CommandMigrationAvailability:
if !m.Legacy.After.Hidden {
return fmt.Errorf("schema_availability_hardening legacy command must migrate exactly from visible to hidden")
}
if m.LegacyFlag != (CommandMigrationFlag{}) {
return fmt.Errorf("schema_availability_hardening must not declare legacy_flag")
}
}
return nil
}
@@ -259,9 +286,8 @@ func (f CommandMigrationFlag) validate() error {
func (s CommandMigrationSchema) validate(kind string) error {
for label, value := range map[string]string{
"product_id": s.ProductID,
"source_tool_id": s.SourceToolID,
"replacement_tool_id": s.ReplacementToolID,
"product_id": s.ProductID,
"source_tool_id": s.SourceToolID,
} {
if !isExactSchemaIdentifier(value) {
return fmt.Errorf("schema %s must be an exact identifier", label)
@@ -270,6 +296,21 @@ func (s CommandMigrationSchema) validate(kind string) error {
if s.Parameters == nil {
return fmt.Errorf("schema parameters must be an array")
}
if kind == CommandMigrationAvailability {
if s.ReplacementToolID != "" || len(s.Parameters) != 0 {
return fmt.Errorf("schema_availability_hardening must not declare a replacement tool or parameter mappings")
}
if s.Availability == nil || s.Availability.Before != "available" || s.Availability.After != "unavailable" {
return fmt.Errorf("schema_availability_hardening requires availability available to unavailable")
}
return nil
}
if !isExactSchemaIdentifier(s.ReplacementToolID) {
return fmt.Errorf("schema replacement_tool_id must be an exact identifier")
}
if s.Availability != nil {
return fmt.Errorf("%s must not declare schema availability", kind)
}
seenFrom := map[string]bool{}
seenTo := map[string]bool{}
seenConstantProperty := map[string]bool{}
@@ -344,6 +385,9 @@ func (m CommandMigration) key() string {
}
func (m CommandMigration) displayKey() string {
if m.Kind == CommandMigrationAvailability {
return fmt.Sprintf("%s %q", m.Kind, m.Legacy.Command)
}
return fmt.Sprintf("%s %q -> %q", m.Kind, m.Legacy.Command, m.Replacement.Command)
}
@@ -401,7 +445,7 @@ func AuthorizeCommandMigrations(
func validateCommandMoveLegacyLeaves(snapshot Snapshot, manifest CommandMigrationManifest) error {
commands := commandIndex(snapshot)
for _, migration := range manifest.Migrations {
if migration.Kind != CommandMigrationMove {
if migration.Kind != CommandMigrationMove && migration.Kind != CommandMigrationAvailability {
continue
}
if _, present := commands[migration.Legacy.Command]; !present {
@@ -409,7 +453,7 @@ func validateCommandMoveLegacyLeaves(snapshot Snapshot, manifest CommandMigratio
}
for _, command := range snapshot.Commands {
if strings.HasPrefix(command.Path, migration.Legacy.Command+" ") {
return fmt.Errorf("command_move legacy command %q must be a leaf", migration.Legacy.Command)
return fmt.Errorf("%s legacy command %q must be a leaf", migration.Kind, migration.Legacy.Command)
}
}
}
@@ -524,6 +568,15 @@ func sameCommandMigrationApproval(left, right CommandMigration) bool {
func matchCommandMigrationPhase(snapshot Snapshot, migration CommandMigration) commandMigrationPhase {
commands := commandIndex(snapshot)
legacy := commandMigrationStateForCommand(commands, migration.Legacy.Command)
if migration.Kind == CommandMigrationAvailability {
if legacy == migration.Legacy.Before {
return commandMigrationBefore
}
if legacy == migration.Legacy.After {
return commandMigrationAfter
}
return commandMigrationPartial
}
replacement := commandMigrationStateForCommand(commands, migration.Replacement.Command)
before := legacy == migration.Legacy.Before && replacement == migration.Replacement.Before
after := legacy == migration.Legacy.After && replacement == migration.Replacement.After
@@ -629,6 +682,10 @@ func commandMigrationAuthorizesChange(current, reference Snapshot, change Change
if change.Kind == "command_became_hidden" && change.Flag == "" {
return true
}
case CommandMigrationAvailability:
if change.Kind == "command_became_hidden" && change.Flag == "" {
return true
}
case CommandMigrationFlagExtraction:
if change.Kind == "flag_became_hidden" && change.Flag == migration.LegacyFlag.Name {
return true
@@ -186,6 +186,7 @@ func TestCrossPlatformCoverageCommandMigrationValidationEdges(t *testing.T) {
want string
}{
{"identifier", CommandMigrationMove, func(s *CommandMigrationSchema) { s.ProductID = "bad/id" }, "exact identifier"},
{"replacement identifier", CommandMigrationMove, func(s *CommandMigrationSchema) { s.ReplacementToolID = "bad/id" }, "replacement_tool_id"},
{"nil parameters", CommandMigrationMove, func(s *CommandMigrationSchema) { s.Parameters = nil }, "must be an array"},
{"bad from", CommandMigrationMove, func(s *CommandMigrationSchema) { s.Parameters[0].From = "bad name" }, "exact parameter"},
{"duplicate from", CommandMigrationMove, func(s *CommandMigrationSchema) { s.Parameters = append(s.Parameters, s.Parameters[0]) }, "duplicates from"},
@@ -241,6 +242,96 @@ func TestCrossPlatformCoverageCommandMigrationValidationEdges(t *testing.T) {
}
}
func TestCrossPlatformCoverageSchemaAvailabilityMigrationLifecycle(t *testing.T) {
pending := availabilityCommandMigrationManifest(CommandMigrationPending)
consumed := availabilityCommandMigrationManifest(CommandMigrationConsumed)
empty := CommandMigrationManifest{Version: CommandMigrationManifestVersion, Migrations: []CommandMigration{}}
before := testSnapshot(
testCommand("dws"),
testCommand("dws devapp"),
testCommand("dws devapp +event-list"),
)
after := testSnapshot(
testCommand("dws"),
testCommand("dws devapp"),
Command{Path: "dws devapp +event-list", Runnable: true, Hidden: true},
)
got, err := AuthorizeCommandMigrations(
after,
map[string]Snapshot{"merge-base": before, "stable": before},
pending,
consumed,
)
if err != nil || len(got) != 1 {
t.Fatalf("availability lifecycle authorizations=%#v error=%v", got, err)
}
report, err := CompareAllWithInterfaceMigrations(
after,
map[string]Snapshot{"merge-base": before, "stable": before},
FlagMigrationManifest{Version: FlagMigrationManifestVersion, Migrations: []FlagMigration{}},
FlagMigrationManifest{Version: FlagMigrationManifestVersion, Migrations: []FlagMigration{}},
pending,
consumed,
)
if err != nil || !report.Compatible {
t.Fatalf("availability interface report compatible=%v error=%v report=%#v", report.Compatible, err, report)
}
if got, err := AuthorizeCommandMigrations(
before,
map[string]Snapshot{"merge-base": before, "stable": before},
empty,
pending,
); err != nil || len(got) != 0 {
t.Fatalf("candidate-added pending migration must remain inert: %#v, %v", got, err)
}
valid := pending.Migrations[0]
for _, test := range []struct {
name string
mutate func(*CommandMigration)
want string
}{
{"legacy path", func(m *CommandMigration) { m.Legacy.Command = "dws devapp *" }, "legacy must be an exact"},
{"replacement", func(m *CommandMigration) { m.Replacement.Command = "dws devapp other" }, "must not declare replacement"},
{"visible after", func(m *CommandMigration) { m.Legacy.After.Hidden = false }, "visible to hidden"},
{"legacy flag", func(m *CommandMigration) { m.LegacyFlag.Name = "legacy" }, "must not declare legacy_flag"},
{"missing availability", func(m *CommandMigration) { m.Schema.Availability = nil }, "requires availability"},
{"wrong availability", func(m *CommandMigration) { m.Schema.Availability.After = "available" }, "requires availability"},
{"parameter mapping", func(m *CommandMigration) {
m.Schema.Parameters = []CommandParameterMigration{{From: "cursor", To: "next-token"}}
}, "must not declare"},
} {
t.Run(test.name, func(t *testing.T) {
migration := cloneCommandMigration(valid)
if valid.Schema.Availability != nil {
change := *valid.Schema.Availability
migration.Schema.Availability = &change
}
test.mutate(&migration)
if err := migration.validate(); err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("availability migration validation error=%v, want %q", err, test.want)
}
})
}
if got := valid.displayKey(); !strings.Contains(got, CommandMigrationAvailability) {
t.Fatalf("availability display key = %q", got)
}
if got := matchCommandMigrationPhase(testSnapshot(testCommand("dws")), valid); got != commandMigrationPartial {
t.Fatalf("missing availability command phase = %q", got)
}
parent := before
parent.Commands = append(append([]Command(nil), before.Commands...), testCommand("dws devapp +event-list detail"))
if err := validateCommandMoveLegacyLeaves(parent, pending); err == nil || !strings.Contains(err.Error(), "must be a leaf") {
t.Fatalf("availability parent command error = %v", err)
}
moveSchema := commandMigrationManifest(CommandMigrationPending).Migrations[0].Schema
moveSchema.Availability = &CommandAvailabilityChange{Before: "available", After: "unavailable"}
if err := moveSchema.validate(CommandMigrationMove); err == nil || !strings.Contains(err.Error(), "must not declare schema availability") {
t.Fatalf("ordinary move availability error = %v", err)
}
}
func TestCrossPlatformCoverageFlagExtractionRejectsRequiredLegacyFlag(t *testing.T) {
for _, test := range []struct {
name string
@@ -686,7 +777,10 @@ func flagExtractionCommandMigrationManifest(state string) CommandMigrationManife
func cloneCommandMigration(source CommandMigration) CommandMigration {
cloned := source
cloned.Schema.Parameters = append([]CommandParameterMigration(nil), source.Schema.Parameters...)
if source.Schema.Parameters != nil {
cloned.Schema.Parameters = make([]CommandParameterMigration, len(source.Schema.Parameters))
copy(cloned.Schema.Parameters, source.Schema.Parameters)
}
for index, parameter := range source.Schema.Parameters {
if parameter.ReplacementConstant == nil {
continue
@@ -703,6 +797,28 @@ func singleCommandMigrationManifest(state string) CommandMigrationManifest {
return manifest
}
func availabilityCommandMigrationManifest(state string) CommandMigrationManifest {
return CommandMigrationManifest{
Version: CommandMigrationManifestVersion,
Migrations: []CommandMigration{{
Kind: CommandMigrationAvailability,
Legacy: CommandMigrationSide{
Command: "dws devapp +event-list",
Before: CommandMigrationState{Present: true, Runnable: true},
After: CommandMigrationState{Present: true, Runnable: true, Hidden: true},
},
Schema: CommandMigrationSchema{
ProductID: "devapp",
SourceToolID: "devapp.shortcut_event_list",
Parameters: []CommandParameterMigration{},
Availability: &CommandAvailabilityChange{Before: "available", After: "unavailable"},
},
State: state,
Reason: "Fail closed until terminal pagination evidence is available.",
}},
}
}
func modifiedCommandManifest(source CommandMigrationManifest) CommandMigrationManifest {
modified := source
modified.Migrations = append([]CommandMigration(nil), source.Migrations...)
@@ -13,6 +13,69 @@ import (
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot"
)
func TestCrossPlatformCoverageSchemaAvailabilityMigrationNormalizesOnlyAvailability(t *testing.T) {
baseline := baselineContract()
current := cloneContract(baseline)
mutateTool(&current, func(tool *toolSchema) { tool.Availability = "unavailable" })
migration := interfacesnapshot.CommandMigration{
Kind: interfacesnapshot.CommandMigrationAvailability,
Legacy: interfacesnapshot.CommandMigrationSide{
Command: "dws doc create",
Before: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true},
After: interfacesnapshot.CommandMigrationState{Present: true, Runnable: true, Hidden: true},
},
Schema: interfacesnapshot.CommandMigrationSchema{
ProductID: "doc",
SourceToolID: "doc.create",
Parameters: []interfacesnapshot.CommandParameterMigration{},
Availability: &interfacesnapshot.CommandAvailabilityChange{Before: "available", After: "unavailable"},
},
State: interfacesnapshot.CommandMigrationPending,
Reason: "Reviewed fail-closed availability hardening.",
}
normalized, err := normalizeSchemaCommandMigrations(baseline, current, []interfacesnapshot.CommandMigration{migration})
if err != nil {
t.Fatal(err)
}
if got := normalized.Products["doc"].Tools["doc.create"].Availability; got != "unavailable" {
t.Fatalf("normalized availability = %q", got)
}
if failures := checkCompatibility(normalized, current); len(failures) != 0 {
t.Fatalf("availability hardening failures = %v", failures)
}
unrelated := cloneContract(current)
mutateTool(&unrelated, func(tool *toolSchema) { tool.Risk = "high" })
normalized, err = normalizeSchemaCommandMigrations(baseline, unrelated, []interfacesnapshot.CommandMigration{migration})
if err != nil {
t.Fatal(err)
}
if failures := strings.Join(checkCompatibility(normalized, unrelated), "\n"); !strings.Contains(failures, "changed risk") {
t.Fatalf("unrelated risk drift was hidden: %q", failures)
}
alreadyAfter := cloneContract(current)
if _, err := normalizeSchemaCommandMigrations(alreadyAfter, current, []interfacesnapshot.CommandMigration{migration}); err != nil {
t.Fatalf("consumed merge-base availability should remain inert: %v", err)
}
wrongPath := cloneContract(current)
mutateTool(&wrongPath, func(tool *toolSchema) { tool.PrimaryCLIPath = "doc other" })
normalized, err = normalizeSchemaCommandMigrations(baseline, wrongPath, []interfacesnapshot.CommandMigration{migration})
if err != nil {
t.Fatal(err)
}
if got := normalized.Products["doc"].Tools["doc.create"].Availability; got != "available" {
t.Fatalf("wrong-path migration changed availability to %q", got)
}
wrong := cloneContract(current)
mutateTool(&wrong, func(tool *toolSchema) { tool.Availability = "available" })
if _, err := normalizeSchemaCommandMigrations(baseline, wrong, []interfacesnapshot.CommandMigration{migration}); err == nil ||
!strings.Contains(err.Error(), "does not match Schema availability") {
t.Fatalf("wrong availability error = %v", err)
}
}
func TestCrossPlatformCoverageSchemaCommandMigrationsAuthorizeOnlyExactProjection(t *testing.T) {
baseline := schemaCommandMigrationContract(false)
current := schemaCommandMigrationContract(true)
+18
View File
@@ -1875,6 +1875,24 @@ func normalizeSchemaCommandMigrations(
normalizedProduct := normalized.Products[migration.Schema.ProductID]
normalizedTool := normalizedProduct.Tools[migration.Schema.SourceToolID]
switch migration.Kind {
case interfacesnapshot.CommandMigrationAvailability:
change := migration.Schema.Availability
if change != nil && oldTool.Availability == change.After && newSource.Availability == change.After {
continue
}
if change == nil || oldTool.Availability != change.Before || newSource.Availability != change.After {
return schemaContract{}, fmt.Errorf(
"approved availability hardening %q does not match Schema availability %q -> %q",
migration.Legacy.Command,
oldTool.Availability,
newSource.Availability,
)
}
if newSource.PrimaryCLIPath != legacyPath {
continue
}
normalizedTool.Availability = newSource.Availability
case interfacesnapshot.CommandMigrationMove:
if newSource.PrimaryCLIPath != replacementPath {
continue