// 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 TestCrossPlatformCoverageSchemaAvailabilityMigrationNormalizesOnlyAvailability(t *testing.T) { baseline := baselineContract() current := cloneContract(baseline) mutateTool(¤t, 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) } compatibilityVisible := migration compatibilityVisible.Legacy.After = compatibilityVisible.Legacy.Before if _, err := normalizeSchemaCommandMigrations(baseline, current, []interfacesnapshot.CommandMigration{compatibilityVisible}); err != nil { t.Fatalf("compatibility-visible availability hardening must authorize the exact Schema transition: %v", err) } if _, err := normalizeSchemaCommandMigrations(baseline, baseline, []interfacesnapshot.CommandMigration{compatibilityVisible}); err == nil || !strings.Contains(err.Error(), "does not match Schema availability") { t.Fatalf("compatibility-visible receipt must not authorize unchanged Schema availability: %v", err) } } 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) } requiredness := interfacesnapshot.FlagMigration{ Kind: interfacesnapshot.FlagMigrationRequirednessChange, Command: "dws report cli-only", Flag: &interfacesnapshot.FlagMigrationSide{ Name: "recipient", Before: interfacesnapshot.FlagMigrationState{Present: true, Type: "string", Scope: "local"}, After: interfacesnapshot.FlagMigrationState{Present: true, Type: "string", Required: true, Scope: "local"}, }, State: interfacesnapshot.FlagMigrationConsumed, Reason: "Exercise requiredness receipts beside command lineage without treating them as renames.", } flagsWithRequiredness := append([]interfacesnapshot.FlagMigration(nil), schemaCommandLineageFlagManifest().Migrations...) flagsWithRequiredness = append(flagsWithRequiredness, requiredness) if _, err := normalizeSchemaCommandMigrationLineage( stable, base, current, flagsWithRequiredness, schemaCommandLineageManifest(interfacesnapshot.CommandMigrationPending).Migrations, ); err != nil { t.Fatalf("requiredness receipt interfered with command lineage: %v", err) } // 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) } }