// Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 (the "License"); package main import ( "bytes" "encoding/json" "errors" "os" "path/filepath" "reflect" "sort" "strings" "testing" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/interfacesnapshot" ) const completeSchemaJSON = `{ "kind":"schema", "level":"catalog", "products":[{ "id":"doc", "tools":[{ "canonical_path":"doc.create", "primary_cli_path":"doc create", "interface_mode":"local", "interface_ref":{"transport":"local"}, "availability":"available", "parameters":{ "title":{ "type":"string", "property":"title", "required":true, "cli_required":true, "interface_type":"string", "default":null, "field_provenance":{} }, "format":{ "type":["string","null"], "property":"format", "required":false, "interface_type":"string", "default":"markdown", "enum":["markdown","text"], "field_provenance":{} } }, "constraints":{"require_one_of":[["title","format"]]}, "positionals":[{ "name":"content", "index":0, "type":"string", "required":false, "description":"original prose" }], "effect":"write", "risk":"medium", "confirmation":"not_required", "idempotency":"unknown", "field_provenance":{} }] }] }` type failingWriter struct{} func (failingWriter) Write([]byte) (int, error) { return 0, errors.New("write failed") } func TestRunSchemaModes(t *testing.T) { directory := t.TempDir() raw := filepath.Join(directory, "raw.json") writeTestFile(t, raw, completeSchemaJSON) var normalized, stderr bytes.Buffer if code := run([]string{"--normalize", raw}, &normalized, &stderr); code != 0 { t.Fatalf("normalize code=%d stderr=%s", code, stderr.String()) } baseline := filepath.Join(directory, "baseline.json") writeTestFile(t, baseline, normalized.String()) var stdout bytes.Buffer stderr.Reset() if code := run([]string{"--check", baseline, "--current", raw}, &stdout, &stderr); code != 0 { t.Fatalf("check code=%d stderr=%s", code, stderr.String()) } if !strings.Contains(stdout.String(), "compatibility check: ok") { t.Fatalf("unexpected check output %q", stdout.String()) } stdout.Reset() stderr.Reset() if code := run([]string{"--merge", baseline, "--current", raw}, &stdout, &stderr); code != 0 { t.Fatalf("merge code=%d stderr=%s", code, stderr.String()) } if !strings.Contains(stdout.String(), `"doc.create"`) { t.Fatalf("unexpected merge output %q", stdout.String()) } empty := filepath.Join(directory, "empty.json") writeTestFile(t, empty, `{"kind":"schema","products":[]}`) stderr.Reset() if code := run([]string{"--check", baseline, "--current", empty}, &stdout, &stderr); code != 2 { t.Fatalf("empty current contract code=%d, want 2", code) } for _, args := range [][]string{ nil, {"--normalize", raw, "--check", baseline}, {"--check", baseline}, {"--normalize", filepath.Join(directory, "missing")}, {"--unknown"}, } { stderr.Reset() if code := run(args, &stdout, &stderr); code != 2 { t.Errorf("run(%v) code=%d, want 2", args, code) } } stderr.Reset() if code := run([]string{"--normalize", raw}, failingWriter{}, &stderr); code != 2 { t.Fatalf("write failure code=%d, want 2", code) } } func TestNormalizeRawFileValidation(t *testing.T) { tests := []struct { name string body string want string }{ {name: "invalid json", body: `{`, want: "unexpected end"}, {name: "wrong kind", body: `{"kind":"other","products":[]}`, want: "unexpected kind"}, {name: "missing products", body: `{"kind":"schema"}`, want: "products array is missing"}, {name: "empty products", body: `{"kind":"schema","products":[]}`, want: "contains no products"}, {name: "empty tools", body: `{"kind":"schema","products":[{"id":"doc","tools":[]}]}`, want: "contains no tools"}, {name: "missing product id", body: `{"kind":"schema","products":[{"tools":[]}]}`, want: "product without id"}, {name: "duplicate product", body: `{"kind":"schema","products":[{"id":"doc"},{"id":"doc"}]}`, want: "duplicate product"}, {name: "compact tool rejected", body: `{"kind":"schema","products":[{"id":"doc","tools":[{"canonical_path":"doc.create","parameters":{},"effect":"write","risk":"medium","confirmation":"not_required","idempotency":"unknown","interface_mode":"local","availability":"available"}]}]}`, want: "not a complete schema --all leaf"}, {name: "invalid required", body: strings.Replace(completeSchemaJSON, `"required":true`, `"required":"yes"`, 1), want: "cannot unmarshal string"}, {name: "incomplete parameter", body: strings.Replace(completeSchemaJSON, `"field_provenance":{}`, `"incomplete":true`, 1), want: "not a complete schema --all parameter"}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { path := filepath.Join(t.TempDir(), "raw.json") writeTestFile(t, path, test.body) _, err := normalizeRawFile(path) if err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("normalizeRawFile() error=%v, want %q", err, test.want) } }) } } func TestNormalizeCompleteSchemaPayload(t *testing.T) { path := filepath.Join(t.TempDir(), "schema.json") writeTestFile(t, path, completeSchemaJSON) contract, err := normalizeRawFile(path) if err != nil { t.Fatal(err) } tool := contract.Products["doc"].Tools["doc.create"] if tool.PrimaryCLIPath != "doc create" || tool.Constraints == "" || tool.Effect != "write" { t.Fatalf("normalized tool contract is incomplete: %#v", tool) } if len(tool.Positionals) != 1 || tool.Positionals[0].Name != "content" { t.Fatalf("normalized positionals = %#v", tool.Positionals) } if got := tool.Parameters["title"]; got.Type != `"string"` || !got.Required || got.Property != "title" || got.InterfaceType != "string" { t.Fatalf("title parameter = %#v", got) } if got := tool.Parameters["format"]; got.Type != `["string","null"]` || got.Default != `"markdown"` { t.Fatalf("format parameter = %#v", got) } } func TestSchemaCompatibilityIgnoresPositionalDescription(t *testing.T) { directory := t.TempDir() baselinePath := filepath.Join(directory, "baseline.json") currentPath := filepath.Join(directory, "current.json") writeTestFile(t, baselinePath, completeSchemaJSON) writeTestFile(t, currentPath, strings.Replace(completeSchemaJSON, "original prose", "edited prose only", 1)) baseline, err := normalizeRawFile(baselinePath) if err != nil { t.Fatal(err) } current, err := normalizeRawFile(currentPath) if err != nil { t.Fatal(err) } if failures := checkCompatibility(baseline, current); len(failures) != 0 { t.Fatalf("positional description edit should be compatible: %v", failures) } } func TestSchemaTypeAndHelpers(t *testing.T) { if got := schemaType(map[string]any{"type": []any{"string", "null"}}); got != `["string","null"]` { t.Fatalf("schemaType(type)=%q", got) } if got := schemaType(map[string]any{"oneOf": []any{"a"}}); got != `oneOf:["a"]` { t.Fatalf("schemaType(oneOf)=%q", got) } if got := schemaType(map[string]any{}); got != "unspecified" { t.Fatalf("schemaType(empty)=%q", got) } if !enumNarrowed([]string{"a", "b"}, []string{"a"}) || enumNarrowed([]string{"a"}, []string{"a", "b"}) { t.Fatal("enum narrowing classification is incorrect") } } func TestSchemaCompatibilityAllowsAdditionsAndLooserInputs(t *testing.T) { baseline := baselineContract() mutateTool(&baseline, func(tool *toolSchema) { tool.DryRun = "" }) current := cloneContract(baseline) mutateParameter(¤t, func(parameter *parameterSchema) { parameter.Required = false parameter.CLIRequired = false parameter.Enum = append(parameter.Enum, "html") }) mutateTool(¤t, func(tool *toolSchema) { tool.Parameters["folder"] = parameterSchema{Type: `"string"`} tool.DryRun = `{"mode":"native"}` }) current.Products["doc"].Tools["doc.read"] = toolSchema{Parameters: map[string]parameterSchema{}} current.Products["sheet"] = productSchema{Tools: map[string]toolSchema{}} if failures := checkCompatibility(baseline, current); len(failures) != 0 { t.Fatalf("compatible additions should pass: %v", failures) } } func TestSchemaCompatibilityAllowsLooserAndAppendedOptionalPositionals(t *testing.T) { baseline := baselineContract() mutateTool(&baseline, func(tool *toolSchema) { tool.Positionals[0].Required = true }) current := cloneContract(baseline) mutateTool(¤t, func(tool *toolSchema) { tool.Positionals[0].Required = false tool.Positionals = append(tool.Positionals, positionalSchema{ Name: "template", Index: 1, Type: "string", }) }) if failures := checkCompatibility(baseline, current); len(failures) != 0 { t.Fatalf("looser and appended optional positionals should pass: %v", failures) } } func TestCompatiblePositionals(t *testing.T) { baseline := []positionalSchema{ {Name: "content", Index: 0, Type: "string", Required: true}, {Name: "format", Index: 1, Type: "string"}, } tests := []struct { name string old []positionalSchema current []positionalSchema compatible bool }{ {name: "unchanged", old: baseline, current: clonePositionals(baseline), compatible: true}, {name: "required becomes optional", old: baseline, current: []positionalSchema{ {Name: "content", Index: 0, Type: "string"}, {Name: "format", Index: 1, Type: "string"}, }, compatible: true}, {name: "append optional", old: baseline, current: append(clonePositionals(baseline), positionalSchema{ Name: "template", Index: 2, Type: "string", }), compatible: true}, {name: "last positional becomes variadic", old: baseline, current: []positionalSchema{ {Name: "content", Index: 0, Type: "string", Required: true}, {Name: "format", Index: 1, Type: "string", Variadic: true}, }, compatible: true}, {name: "removed", old: baseline, current: clonePositionals(baseline[:1])}, {name: "renamed", old: baseline, current: []positionalSchema{ {Name: "body", Index: 0, Type: "string", Required: true}, {Name: "format", Index: 1, Type: "string"}, }}, {name: "reindexed", old: baseline, current: []positionalSchema{ {Name: "content", Index: 1, Type: "string", Required: true}, {Name: "format", Index: 2, Type: "string"}, }}, {name: "retyped", old: baseline, current: []positionalSchema{ {Name: "content", Index: 0, Type: "number", Required: true}, {Name: "format", Index: 1, Type: "string"}, }}, {name: "optional becomes required", old: baseline, current: []positionalSchema{ {Name: "content", Index: 0, Type: "string", Required: true}, {Name: "format", Index: 1, Type: "string", Required: true}, }}, {name: "append required", old: baseline, current: append(clonePositionals(baseline), positionalSchema{ Name: "template", Index: 2, Type: "string", Required: true, })}, {name: "variadic becomes fixed", old: []positionalSchema{ {Name: "content", Index: 0, Type: "string", Variadic: true}, }, current: []positionalSchema{ {Name: "content", Index: 0, Type: "string"}, }}, {name: "append after variadic", old: []positionalSchema{ {Name: "content", Index: 0, Type: "string", Variadic: true}, }, current: []positionalSchema{ {Name: "content", Index: 0, Type: "string", Variadic: true}, {Name: "format", Index: 1, Type: "string"}, }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { if got := compatiblePositionals(test.old, test.current); got != test.compatible { t.Fatalf("compatiblePositionals() = %t, want %t", got, test.compatible) } }) } } func TestSchemaCompatibilityRejectsContractDrift(t *testing.T) { tests := []struct { name string want string mutate func(*schemaContract) }{ {name: "removed product", want: "historical schema product", mutate: func(contract *schemaContract) { delete(contract.Products, "doc") }}, {name: "removed tool", want: "historical schema tool", mutate: func(contract *schemaContract) { delete(contract.Products["doc"].Tools, "doc.create") }}, {name: "removed parameter", want: "lost parameter", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { delete(tool.Parameters, "title") }) }}, {name: "changed type", want: "changed type", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.Type = `"number"` }) }}, {name: "new required", want: "newly required", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.Required = true }) }}, {name: "new cli required", want: "newly cli_required", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.CLIRequired = true }) }}, {name: "changed required when", want: "changed required_when", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.RequiredWhen = "scope=team" }) }}, {name: "changed property", want: "changed property", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.Property = "subject" }) }}, {name: "cleared property without a reviewed exclusion", want: "changed property", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.Property = "" parameter.PropertySource = "flag_name_inference" }) }}, {name: "redirected property despite a reviewed exclusion", want: "changed property", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.Property = "subject" parameter.PropertySource = propertySourceReviewedMappingExclusion }) }}, {name: "changed interface type", want: "changed interface_type", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.InterfaceType = "integer" }) }}, {name: "changed default", want: "changed default", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.Default = `"html"` }) }}, {name: "changed interface default", want: "changed interface_default", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.InterfaceDefault = `"html"` }) }}, {name: "changed format", want: "changed format", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.Format = "uri" }) }}, {name: "narrowed enum", want: "narrowed enum", mutate: func(contract *schemaContract) { mutateParameter(contract, func(parameter *parameterSchema) { parameter.Enum = []string{"markdown"} }) }}, {name: "changed primary cli path", want: "changed primary_cli_path", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.PrimaryCLIPath = "doc make" }) }}, {name: "changed interface mode", want: "changed interface_mode", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceMode = "mcp" }) }}, {name: "changed constraints", want: "changed constraints", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.Constraints = `{}` }) }}, {name: "changed positionals", want: "changed positionals", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.Positionals[0].Name = "id" }) }}, {name: "changed interface mapping", want: "changed interface_ref", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceRef = `{"transport":"mcp"}` }) }}, {name: "changed availability", want: "changed availability", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.Availability = "unavailable" }) }}, {name: "changed confirmation", want: "changed confirmation", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.Confirmation = "user_required" }) }}, {name: "changed risk", want: "changed risk", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.Risk = "high" }) }}, {name: "changed effect", want: "changed effect", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.Effect = "destructive" }) }}, {name: "changed idempotency", want: "changed idempotency", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.Idempotency = "idempotent" }) }}, {name: "removed dry run", want: "changed or removed dry_run", mutate: func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.DryRun = "" }) }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { current := baselineContract() test.mutate(¤t) failures := strings.Join(checkCompatibility(baselineContract(), current), "\n") if !strings.Contains(failures, test.want) { t.Fatalf("failures=%q, want %q", failures, test.want) } }) } } func TestMergeContracts(t *testing.T) { historical := baselineContract() current := cloneContract(historical) mutateTool(¤t, func(tool *toolSchema) { tool.Parameters["folder"] = parameterSchema{Type: `"string"`} }) merged, failures := mergeContracts(historical, current) if len(failures) != 0 || merged.Products["doc"].Tools["doc.create"].Parameters["folder"].Type == "" { t.Fatalf("merge=%v failures=%v", merged, failures) } mutateParameter(¤t, func(parameter *parameterSchema) { parameter.Type = `"number"` }) if _, failures := mergeContracts(historical, current); len(failures) == 0 { t.Fatal("incompatible merge unexpectedly passed") } } // registerReviewedParameterTypeFixture puts a fixture entry in the reviewed // table for the duration of one test, so the behaviour tests exercise the real // lookup instead of depending on whichever production entries happen to exist. func registerReviewedParameterTypeFixture(t *testing.T, change parameterTypeChange) { t.Helper() if _, exists := reviewedParameterTypeChanges[change]; exists { t.Fatalf("fixture %+v collides with a production entry", change) } reviewedParameterTypeChanges[change] = struct{}{} t.Cleanup(func() { delete(reviewedParameterTypeChanges, change) }) } func reviewedFormatTypeFixture() parameterTypeChange { return parameterTypeChange{ ToolPath: "doc/doc.create", Parameter: "format", From: `"string"`, To: `"integer"`, } } const docCreateFormatTypeFailure = `schema tool "doc/doc.create" parameter "format" changed type` func assertSchemaFailureContains(t *testing.T, failures []string, want string) { t.Helper() for _, failure := range failures { if strings.Contains(failure, want) { return } } t.Fatalf("failures %v do not contain %q", failures, want) } func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChange(t *testing.T) { baseline := cloneContract(baselineContract()) current := cloneContract(baseline) mutateParameter(¤t, func(parameter *parameterSchema) { parameter.Type = `"integer"` }) // Without an entry the migration is still a blocking change. assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure) registerReviewedParameterTypeFixture(t, reviewedFormatTypeFixture()) if failures := checkCompatibility(baseline, current); len(failures) != 0 { t.Fatalf("a reviewed parameter type migration should pass: %v", failures) } } func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChangeIsDirectionSensitive(t *testing.T) { registerReviewedParameterTypeFixture(t, reviewedFormatTypeFixture()) // The reverse migration shares tool and parameter but not direction, so the // string->integer entry must not admit it. baseline := cloneContract(baselineContract()) mutateParameter(&baseline, func(parameter *parameterSchema) { parameter.Type = `"integer"` }) current := cloneContract(baselineContract()) assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure) } func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChangeRejectsOtherToolsAndParameters(t *testing.T) { baseline := cloneContract(baselineContract()) current := cloneContract(baseline) mutateParameter(¤t, func(parameter *parameterSchema) { parameter.Type = `"integer"` }) // Same parameter and direction, different tool. other := reviewedFormatTypeFixture() other.ToolPath = "doc/doc.elsewhere" registerReviewedParameterTypeFixture(t, other) assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure) // Same tool and direction, different parameter. elsewhere := reviewedFormatTypeFixture() elsewhere.Parameter = "title" registerReviewedParameterTypeFixture(t, elsewhere) assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure) } // A reviewed migration is only accepted when the rest of the parameter's // contract held still. Each case bundles one unrelated regression with the // reviewed type change and requires the type failure to reappear, so the // exemption cannot carry a second change in behind it. func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChangeRejectsBundledRegression(t *testing.T) { registerReviewedParameterTypeFixture(t, reviewedFormatTypeFixture()) tests := []struct { name string mutate func(*parameterSchema) }{ {name: "changed default", mutate: func(p *parameterSchema) { p.Default = `"text"` }}, {name: "changed interface_default", mutate: func(p *parameterSchema) { p.InterfaceDefault = `"text"` }}, {name: "changed format", mutate: func(p *parameterSchema) { p.Format = "uri" }}, {name: "redirected property", mutate: func(p *parameterSchema) { p.Property = "renamed" }}, {name: "redirected interface_type", mutate: func(p *parameterSchema) { p.InterfaceType = "integer" }}, {name: "newly required", mutate: func(p *parameterSchema) { p.Required = true }}, {name: "newly cli_required", mutate: func(p *parameterSchema) { p.CLIRequired = true }}, {name: "changed required_when", mutate: func(p *parameterSchema) { p.RequiredWhen = "always" }}, {name: "narrowed enum", mutate: func(p *parameterSchema) { p.Enum = []string{"markdown"} }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { baseline := cloneContract(baselineContract()) current := cloneContract(baseline) mutateParameter(¤t, func(parameter *parameterSchema) { parameter.Type = `"integer"` test.mutate(parameter) }) assertSchemaFailureContains(t, checkCompatibility(baseline, current), docCreateFormatTypeFailure) }) } } // Every drift below is individually *compatible* — on its own it produces no // failure at all — which is precisely why the carve-out cannot be keyed on "the // gate recorded no other failure for this parameter". Bundled with a reviewed // type migration they must still be rejected: none of them were reviewed under // that entry, and admitting them would make the exemption wider than what the // table documents. // // Each case first asserts that the drift alone really is compatible, so a future // rule change that starts reporting it turns into an explicit failure here // rather than quietly turning this test into a duplicate of the bundled- // regression cases above. func TestCrossPlatformCoverageSchemaCompatReviewedParameterTypeChangeRejectsIndividuallyCompatibleDrift(t *testing.T) { registerReviewedParameterTypeFixture(t, reviewedFormatTypeFixture()) tests := []struct { name string baseline func(*parameterSchema) current func(*parameterSchema) }{ { name: "relaxed required", baseline: func(p *parameterSchema) { p.Required = true }, current: func(p *parameterSchema) { p.Required = false }, }, { name: "relaxed cli_required", baseline: func(p *parameterSchema) { p.CLIRequired = true }, current: func(p *parameterSchema) { p.CLIRequired = false }, }, { name: "cleared required_when", baseline: func(p *parameterSchema) { p.RequiredWhen = "always" }, current: func(p *parameterSchema) { p.RequiredWhen = "" }, }, { name: "widened enum", current: func(p *parameterSchema) { p.Enum = []string{"html", "markdown", "text"} }, }, { name: "cleared interface_type", baseline: func(p *parameterSchema) { p.InterfaceType = "string" }, current: func(p *parameterSchema) { p.InterfaceType = "" }, }, { name: "cleared property through reviewed mapping exclusion", current: func(p *parameterSchema) { p.Property = "" p.PropertySource = propertySourceReviewedMappingExclusion }, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { baseline := cloneContract(baselineContract()) if test.baseline != nil { mutateParameter(&baseline, test.baseline) } driftOnly := cloneContract(baseline) mutateParameter(&driftOnly, test.current) if failures := checkCompatibility(baseline, driftOnly); len(failures) != 0 { t.Fatalf("前提不成立:该变化本身已不兼容,用例测不到相等性守卫: %v", failures) } bundled := cloneContract(baseline) mutateParameter(&bundled, func(parameter *parameterSchema) { parameter.Type = `"integer"` test.current(parameter) }) assertSchemaFailureContains(t, checkCompatibility(baseline, bundled), docCreateFormatTypeFailure) }) } } // The behaviour tests above register their own fixtures, so they cannot catch a // production entry whose type values are spelled in the wrong form. That mistake // disables the exemption silently and only a real gate run reports it — exactly // how reviewedInterfaceRefRedirect was broken twice. Recompute both values // through schemaType instead of trusting the spelling in the table. func TestCrossPlatformCoverageReviewedParameterTypeChangeKeysAreCanonical(t *testing.T) { if len(reviewedParameterTypeChanges) == 0 { t.Fatal("豁免表为空:若确已清空,请连这条守卫一起删除") } for change := range reviewedParameterTypeChanges { if strings.Count(change.ToolPath, "/") != 1 || strings.HasPrefix(change.ToolPath, "/") || strings.HasSuffix(change.ToolPath, "/") { t.Errorf("tool path %q 不是 \"/\" 形态", change.ToolPath) } if strings.TrimSpace(change.Parameter) != change.Parameter || change.Parameter == "" || strings.HasPrefix(change.Parameter, "-") { t.Errorf("%s: 参数名应是 Schema 里的裸名,登记为 %q", change.ToolPath, change.Parameter) } if change.From == change.To { t.Errorf("%s %s: From 与 To 相同,不构成迁移", change.ToolPath, change.Parameter) } for _, side := range []struct { label string value string }{{"From", change.From}, {"To", change.To}} { var name string if err := json.Unmarshal([]byte(side.value), &name); err != nil { t.Errorf("%s %s 的 %s=%q 不是合法的 JSON type 值(schemaType 产出带引号的形态,如 `\"string\"`): %v", change.ToolPath, change.Parameter, side.label, side.value, err) continue } if got := schemaType(map[string]any{"type": name}); got != side.value { t.Errorf("%s %s 的 %s 不是规范形态\n 登记: %s\n 规范: %s", change.ToolPath, change.Parameter, side.label, side.value, got) } // 复算只能证明引号形态对,证明不了类型名本身没拼错——"integar" 同样 // 能通过复算。JSON Schema 的 type 取值是封闭集合,直接校验。 switch name { case "string", "integer", "number", "boolean", "array", "object", "null": default: t.Errorf("%s %s 的 %s=%q 不是 JSON Schema 的合法 type 名", change.ToolPath, change.Parameter, side.label, name) } } } } func baselineContract() schemaContract { return schemaContract{Version: schemaContractVersion, Products: map[string]productSchema{ "doc": {Tools: map[string]toolSchema{ "doc.create": { PrimaryCLIPath: "doc create", InterfaceMode: "local", InterfaceRef: `{"transport":"local"}`, Availability: "available", Parameters: map[string]parameterSchema{ "title": { Type: `"string"`, Property: "title", InterfaceType: "string", }, "format": { Type: `"string"`, Property: "format", InterfaceType: "string", Default: `"markdown"`, Enum: []string{"markdown", "text"}, }, }, Constraints: `{"require_one_of":[["title","format"]]}`, Positionals: []positionalSchema{{ Name: "content", Index: 0, Type: "string", }}, DryRun: `{"mode":"native"}`, Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, }}, }} } func mutateTool(contract *schemaContract, mutate func(*toolSchema)) { product := contract.Products["doc"] tool := product.Tools["doc.create"] mutate(&tool) product.Tools["doc.create"] = tool contract.Products["doc"] = product } func mutateParameter(contract *schemaContract, mutate func(*parameterSchema)) { mutateTool(contract, func(tool *toolSchema) { parameter := tool.Parameters["format"] mutate(¶meter) tool.Parameters["format"] = parameter }) } func clonePositionals(source []positionalSchema) []positionalSchema { return append([]positionalSchema(nil), source...) } func writeTestFile(t *testing.T, path, body string) { t.Helper() if err := os.WriteFile(path, []byte(body), 0o600); err != nil { t.Fatal(err) } } // TestCrossPlatformCoverageSchemaCompatMCPRetirementAndConstraintExpansion covers // the MCP pin retirement allowance (clearing interface_type) and declare≡execute // constraint member expansion used by the platform coverage gate. func TestCrossPlatformCoverageSchemaCompatMCPRetirementAndConstraintExpansion(t *testing.T) { baseline := baselineContract() current := cloneContract(baseline) mutateParameter(¤t, func(parameter *parameterSchema) { parameter.InterfaceType = "" }) mutateTool(¤t, func(tool *toolSchema) { tool.Constraints = `{"require_one_of":[["title","format","legacy-format"]],"mutually_exclusive":[["title","format","legacy-title"]]}` }) // Baseline has require_one_of only; a new mutually-exclusive group that // restricts two historical public parameters must remain incompatible. if failures := checkCompatibility(baseline, current); len(failures) == 0 { t.Fatal("adding a new constraint group must fail compatibility") } // Match baseline shape: expand require_one_of members only, clear interface_type. current = cloneContract(baseline) mutateParameter(¤t, func(parameter *parameterSchema) { parameter.InterfaceType = "" }) mutateTool(¤t, func(tool *toolSchema) { tool.Constraints = `{"require_one_of":[["title","format","legacy-format"]]}` }) if failures := checkCompatibility(baseline, current); len(failures) != 0 { t.Fatalf("clearing interface_type and expanding constraint members should pass: %v", failures) } // Hidden-sibling expansion from empty historical constraints remains allowed. emptyConstraints := cloneContract(baseline) mutateTool(&emptyConstraints, func(tool *toolSchema) { tool.Constraints = "" title := tool.Parameters["title"] title.Required = true tool.Parameters["title"] = title delete(tool.Parameters, "format") }) expanded := cloneContract(emptyConstraints) mutateTool(&expanded, func(tool *toolSchema) { tool.Constraints = `{"require_one_of":[["title","hidden-alias"]],"mutually_exclusive":[["title","hidden-alias"]]}` title := tool.Parameters["title"] title.Required = false tool.Parameters["title"] = title }) if failures := checkCompatibility(emptyConstraints, expanded); len(failures) != 0 { t.Fatalf("hidden-sibling constraint expansion should pass: %v", failures) } if groups, ok := parseConstraintGroups(""); !ok || len(groups["require_one_of"]) != 0 { t.Fatalf("empty constraints parse = %#v ok=%v", groups, ok) } if !stringSetContainsAll(map[string]bool{"a": true, "b": true}, map[string]bool{"a": true}) || stringSetContainsAll(map[string]bool{"a": true}, map[string]bool{"a": true, "b": true}) { t.Fatal("stringSetContainsAll classification is incorrect") } // compatibleHiddenSiblingConstraintExpansion false branches. if compatibleHiddenSiblingConstraintExpansion( toolSchema{Constraints: "", Parameters: map[string]parameterSchema{"a": {Required: true}}}, toolSchema{Constraints: "{", Parameters: map[string]parameterSchema{"a": {}}}, ) { t.Fatal("invalid projected constraints must not expand") } oldRequired := toolSchema{ Constraints: "", Parameters: map[string]parameterSchema{"a": {Required: true}}, } if compatibleHiddenSiblingConstraintExpansion(oldRequired, toolSchema{ Constraints: `{"require_together":[["a","b"]],"require_one_of":[["a","hidden"]]}`, Parameters: map[string]parameterSchema{"a": {}}, }) { t.Fatal("require_together projection must not expand") } if compatibleHiddenSiblingConstraintExpansion(oldRequired, toolSchema{ Constraints: `{"require_one_of":[["a"]]}`, Parameters: map[string]parameterSchema{"a": {}}, }) { t.Fatal("single-member group must not expand") } if compatibleHiddenSiblingConstraintExpansion(oldRequired, toolSchema{ Constraints: `{"require_one_of":[["a","","hidden"]]}`, Parameters: map[string]parameterSchema{"a": {}}, }) { t.Fatal("empty constraint member must not expand") } if compatibleHiddenSiblingConstraintExpansion(oldRequired, toolSchema{ Constraints: `{"require_one_of":[["hidden","ghost"]]}`, Parameters: map[string]parameterSchema{"a": {}}, }) { t.Fatal("unpublished-only group must not expand") } if compatibleHiddenSiblingConstraintExpansion(oldRequired, toolSchema{ Constraints: `{"require_one_of":[["a","b"]]}`, Parameters: map[string]parameterSchema{"a": {}, "b": {}}, }) { t.Fatal("published-only group must not expand") } if compatibleHiddenSiblingConstraintExpansion(oldRequired, toolSchema{ Constraints: `{"require_one_of":[["a","hidden"]]}`, Parameters: map[string]parameterSchema{"a": {Required: true}}, }) { t.Fatal("still-required sole published member must not expand") } // Changing interface_type to a different non-empty value remains incompatible. typed := cloneContract(baseline) mutateParameter(&typed, func(parameter *parameterSchema) { parameter.InterfaceType = "integer" }) if failures := checkCompatibility(baseline, typed); !strings.Contains(strings.Join(failures, "\n"), "changed interface_type") { t.Fatalf("non-empty interface_type change should fail: %v", failures) } } func TestCrossPlatformCoverageSchemaCompatAdditiveConstraintEvolution(t *testing.T) { oldTool := toolSchema{ Parameters: map[string]parameterSchema{ "target": {}, "limit": {}, }, Constraints: `{"require_one_of":[["target","legacy-target"]]}`, } compatible := oldTool compatible.Constraints = `{"require_one_of":[["target","legacy-target","new-target"]],"mutually_exclusive":[["target","new-target"],["new-a","new-b"]],"require_together":[["new-a","new-b"]]}` if !compatibleAdditiveConstraintEvolution(oldTool, compatible) { t.Fatal("member expansion and additive alias-only groups must remain compatible") } twoHistorical := compatible twoHistorical.Constraints = `{"require_one_of":[["target","legacy-target","new-target"]],"mutually_exclusive":[["target","limit","new-target"]]}` if compatibleAdditiveConstraintEvolution(oldTool, twoHistorical) { t.Fatal("new mutex group restricting two historical parameters must fail") } newRequirement := compatible newRequirement.Constraints = `{"require_one_of":[["target","legacy-target","new-target"],["new-a","new-b"]]}` if compatibleAdditiveConstraintEvolution(oldTool, newRequirement) { t.Fatal("new require_one_of group must fail") } requireHistoricalTogether := compatible requireHistoricalTogether.Constraints = `{"require_one_of":[["target","legacy-target","new-target"]],"require_together":[["limit","new-limit"]]}` if compatibleAdditiveConstraintEvolution(oldTool, requireHistoricalTogether) { t.Fatal("new require_together group containing a historical parameter must fail") } removedOldGroup := compatible removedOldGroup.Constraints = `{"mutually_exclusive":[["new-a","new-b"]]}` if compatibleAdditiveConstraintEvolution(oldTool, removedOldGroup) { t.Fatal("removing a historical group must fail") } invalid := compatible invalid.Constraints = "{" if compatibleAdditiveConstraintEvolution(oldTool, invalid) { t.Fatal("invalid constraints must fail closed") } emptyHistoricalGroup := oldTool emptyHistoricalGroup.Constraints = `{"require_one_of":[[]]}` if compatibleAdditiveConstraintEvolution(emptyHistoricalGroup, compatible) { t.Fatal("empty historical constraint group must fail closed") } historicalMutex := oldTool historicalMutex.Constraints = `{"mutually_exclusive":[["target","legacy-target"]]}` historicalMutexExpanded := oldTool historicalMutexExpanded.Constraints = `{"mutually_exclusive":[["target","legacy-target","limit"]]}` if compatibleAdditiveConstraintEvolution(historicalMutex, historicalMutexExpanded) { t.Fatal("adding a historical parameter to an existing mutex group must fail") } historicalTogether := oldTool historicalTogether.Constraints = `{"require_together":[["target","legacy-target"]]}` historicalTogetherExpanded := oldTool historicalTogetherExpanded.Constraints = `{"require_together":[["target","legacy-target","limit"]]}` if compatibleAdditiveConstraintEvolution(historicalTogether, historicalTogetherExpanded) { t.Fatal("adding a historical parameter to an existing require-together group must fail") } historicalOneOf := oldTool historicalOneOf.Constraints = `{"require_one_of":[["target","legacy-target","limit"]]}` if !compatibleAdditiveConstraintEvolution(oldTool, historicalOneOf) { t.Fatal("adding a historical parameter to require-one-of only loosens the contract") } gateBaseline := baselineContract() mutateTool(&gateBaseline, func(tool *toolSchema) { tool.Parameters["folder"] = parameterSchema{Type: `"string"`} tool.Constraints = `{"mutually_exclusive":[["title","format"]]}` }) gateCurrent := cloneContract(gateBaseline) mutateTool(&gateCurrent, func(tool *toolSchema) { tool.Constraints = `{"mutually_exclusive":[["title","format","folder"]]}` }) if failures := checkCompatibility(gateBaseline, gateCurrent); len(failures) == 0 { t.Fatal("compatibility gate must reject an existing mutex group gaining a historical parameter") } multipleHistoricalGroups := oldTool multipleHistoricalGroups.Constraints = `{"require_one_of":[["target"],["limit"]]}` multipleExpandedGroups := oldTool multipleExpandedGroups.Constraints = `{"require_one_of":[["target","limit"],["limit","new-limit"]]}` if !compatibleAdditiveConstraintEvolution(multipleHistoricalGroups, multipleExpandedGroups) { t.Fatal("each historical group must match a distinct expanded group") } } func TestCrossPlatformCoverageSchemaCompatReviewedConstraintTransition(t *testing.T) { const toolPath = "doc/doc.shortcut_import" oldTool := toolSchema{Constraints: `{"require_one_of":[["folder","workspace"]]}`} newTool := oldTool newTool.Constraints = "" if !compatibleReviewedConstraintTransition(toolPath, oldTool, newTool) { t.Fatal("reviewed doc import target removal must be accepted") } if failures := checkToolCompatibility(toolPath, oldTool, newTool); len(failures) != 0 { t.Fatalf("reviewed doc import constraint transition failed: %v", failures) } for _, test := range []struct { name string path string old string new string }{ {name: "unlisted tool", path: "doc/doc.other", old: oldTool.Constraints, new: ""}, {name: "unlisted source", path: toolPath, old: `{"require_one_of":[["folder","workspace","name"]]}`, new: ""}, {name: "unlisted target", path: toolPath, old: oldTool.Constraints, new: `{}`}, } { t.Run(test.name, func(t *testing.T) { if compatibleReviewedConstraintTransition(test.path, toolSchema{Constraints: test.old}, toolSchema{Constraints: test.new}) { t.Fatal("unreviewed constraint transition unexpectedly passed") } }) } } // Clearing a property through the reviewed mapping exclusion table is the one // accepted shape, mirroring the interface_type retirement allowance. A leaf // whose backing RPC moved to a nested payload has no honest flat property to // publish; the alternatives are naming a field the request no longer contains, // or letting flag_name_inference publish a name that appears in no request. func TestCrossPlatformCoverageSchemaCompatPropertyClearingExclusion(t *testing.T) { baseline := baselineContract() // Accepted: non-empty -> empty, resolved through a reviewed exclusion. current := cloneContract(baseline) mutateParameter(¤t, func(parameter *parameterSchema) { parameter.Property = "" parameter.PropertySource = propertySourceReviewedMappingExclusion }) if failures := checkCompatibility(baseline, current); len(failures) != 0 { t.Fatalf("clearing property through a reviewed exclusion should pass: %v", failures) } // Every neighbouring shape must stay incompatible, so the carve-out cannot // be widened by accident. for _, tc := range []struct { name string property string source string }{ {"cleared by inference", "", "flag_name_inference"}, {"cleared by a native annotation", "", "native_annotation"}, {"cleared with no recorded source", "", ""}, {"redirected to another non-empty value", "cellStyles", propertySourceReviewedMappingExclusion}, } { t.Run(tc.name, func(t *testing.T) { drifted := cloneContract(baseline) mutateParameter(&drifted, func(parameter *parameterSchema) { parameter.Property = tc.property parameter.PropertySource = tc.source }) failures := checkCompatibility(baseline, drifted) if len(failures) == 0 { t.Fatal("must remain incompatible") } var sawProperty bool for _, failure := range failures { if strings.Contains(failure, "changed property") { sawProperty = true } } if !sawProperty { t.Fatalf("expected a changed property failure, got %v", failures) } }) } // A parameter that never published a property must not gain one silently // just because it carries an exclusion source. populated := cloneContract(baseline) mutateParameter(&populated, func(parameter *parameterSchema) { parameter.Property = "" }) gained := cloneContract(populated) mutateParameter(&gained, func(parameter *parameterSchema) { parameter.Property = "backgroundColors" parameter.PropertySource = propertySourceReviewedMappingExclusion }) if failures := checkCompatibility(populated, gained); len(failures) == 0 { t.Fatal("populating a previously empty property must stay incompatible") } } // Repointing a leaf at a different backing RPC is accepted only when the // CLI-facing contract is provably unchanged. interface_ref is audit metadata; // nothing reads it at runtime, so a stale value misinforms a reader rather than // misrouting a call. The gate is that no other compatibility check for the tool // failed — that is the operative meaning of "the contract is unchanged". // reviewedInterfaceRefRedirect 的键值必须是 parseTool 经 canonicalRawJSON 实际产出 // 的紧凑 JSON。这条守卫是必要的:该表先前误登记为裸 RPC 名("update_range"),后又 // 误登记为带空格的美化 JSON,两次都让豁免完全失效——而上面的用例是用测试自己注册 // 的值断言的,所以两次都通过了,只有真实门禁才报错。这里用 canonicalRawJSON 复算 // 期望值,把格式锚定到生产代码而不是作者的书写习惯。 func TestCrossPlatformCoverageReviewedRedirectKeysAreCanonicalJSON(t *testing.T) { canon := func(raw string) string { got, err := canonicalRawJSON(json.RawMessage(raw)) if err != nil { t.Fatalf("canonicalRawJSON(%s): %v", raw, err) } return got } if len(reviewedInterfaceRefRedirect) == 0 { t.Fatal("redirect allowlist 为空:若确已清空,请同时删除这条守卫") } for toolPath, pairs := range reviewedInterfaceRefRedirect { if len(pairs) == 0 { t.Errorf("%s: 空的 redirect 表项没有意义", toolPath) } for oldRef, newRef := range pairs { if got := canon(oldRef); got != oldRef { t.Errorf("%s: old ref 不是规范形态\n 登记: %s\n 规范: %s", toolPath, oldRef, got) } if got := canon(newRef); got != newRef { t.Errorf("%s: new ref 不是规范形态\n 登记: %s\n 规范: %s", toolPath, newRef, got) } if oldRef == newRef { t.Errorf("%s: old 与 new 相同,不构成 redirect", toolPath) } } } } func TestCrossPlatformCoverageSchemaCompatInterfaceRefRedirect(t *testing.T) { // The redirect carve-out only accepts an explicitly reviewed tool + old→new // ref pair. Register the fixture's path for the duration of this test so the // accepted shapes below exercise the allowlist rather than a blanket rule. const fixturePath = "doc/doc.create" oldRef := `{"product_id":"sheet","rpc_name":"update_range"}` newRef := `{"product_id":"sheet","rpc_name":"set_cell_range"}` reviewedInterfaceRefRedirect[fixturePath] = map[string]string{oldRef: newRef} t.Cleanup(func() { delete(reviewedInterfaceRefRedirect, fixturePath) }) // The baseline fixture is interface_mode=local; the carve-out only applies to // mcp-backed leaves, so establish an mcp baseline first. baseline := cloneContract(baselineContract()) mutateTool(&baseline, func(tool *toolSchema) { tool.InterfaceMode = interfaceModeMCP tool.InterfaceRef = oldRef }) // Accepted: mcp -> mcp, both refs non-empty, the pair is reviewed, nothing // else changed. redirected := cloneContract(baseline) mutateTool(&redirected, func(tool *toolSchema) { tool.InterfaceRef = newRef }) if failures := checkCompatibility(baseline, redirected); len(failures) != 0 { t.Fatalf("a reviewed interface_ref redirect should pass: %v", failures) } // Paired with a reviewed property clearing, which is the realistic shape: // a leaf moving to a nested payload loses its flat property names. withCleared := cloneContract(redirected) mutateParameter(&withCleared, func(parameter *parameterSchema) { parameter.Property = "" parameter.PropertySource = propertySourceReviewedMappingExclusion }) if failures := checkCompatibility(baseline, withCleared); len(failures) != 0 { t.Fatalf("redirect plus reviewed property clearing should pass: %v", failures) } // Rejected shapes. Each must still report the redirect, so a surface change // cannot ride along behind a backend move. for _, tc := range []struct { name string mutate func(*schemaContract) }{ {"ref removed rather than redirected", func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceRef = "" }) }}, {"mode moved to composite", func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceMode = "composite" tool.InterfaceRef = "" }) }}, {"mode moved away from mcp while the ref is redirected", func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceMode = "local" tool.InterfaceRef = `{"product_id":"sheet","rpc_name":"set_cell_range"}` }) }}, {"a parameter became required", func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceRef = `{"product_id":"sheet","rpc_name":"set_cell_range"}` }) mutateParameter(contract, func(parameter *parameterSchema) { parameter.Required = true }) }}, {"a parameter type moved", func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceRef = `{"product_id":"sheet","rpc_name":"set_cell_range"}` }) mutateParameter(contract, func(parameter *parameterSchema) { parameter.Type = "integer" }) }}, {"a property was cleared without a reviewed exclusion", func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceRef = `{"product_id":"sheet","rpc_name":"set_cell_range"}` }) mutateParameter(contract, func(parameter *parameterSchema) { parameter.Property = "" parameter.PropertySource = "flag_name_inference" }) }}, {"a confirmation gate moved", func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceRef = `{"product_id":"sheet","rpc_name":"set_cell_range"}` tool.Confirmation = "user_required" }) }}, {"a parameter was dropped", func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceRef = newRef delete(tool.Parameters, "title") }) }}, // The allowlist itself must be load-bearing: a redirect that is not an // exact reviewed tool + old→new pair stays incompatible even when the rest // of the contract is untouched. Schema shape cannot prove two RPCs share // permissions, error taxonomy, or side effects. {"redirect to a ref that is not the reviewed target", func(contract *schemaContract) { mutateTool(contract, func(tool *toolSchema) { tool.InterfaceRef = `{"product_id":"sheet","rpc_name":"some_other_rpc"}` }) }}, } { t.Run(tc.name, func(t *testing.T) { drifted := cloneContract(baseline) tc.mutate(&drifted) failures := checkCompatibility(baseline, drifted) var sawRef bool for _, failure := range failures { if strings.Contains(failure, "changed interface_ref") { sawRef = true } } if !sawRef { t.Fatalf("expected the redirect to stay reported, got %v", failures) } }) } // The allowlist must be load-bearing: the exact same old→new pair on a tool // with no reviewed entry stays incompatible. Asserted outside the table // because the registration has to survive until checkCompatibility runs. t.Run("redirect on a tool absent from the reviewed allowlist", func(t *testing.T) { saved := reviewedInterfaceRefRedirect[fixturePath] delete(reviewedInterfaceRefRedirect, fixturePath) defer func() { reviewedInterfaceRefRedirect[fixturePath] = saved }() unlisted := cloneContract(baseline) mutateTool(&unlisted, func(tool *toolSchema) { tool.InterfaceRef = newRef }) failures := checkCompatibility(baseline, unlisted) var sawRef bool for _, failure := range failures { if strings.Contains(failure, "changed interface_ref") { sawRef = true } } if !sawRef { t.Fatalf("an unlisted tool must not get the redirect carve-out, got %v", failures) } }) } func TestCrossPlatformCoverageSchemaFlagMigrationNormalizesExactRename(t *testing.T) { baseline := schemaFlagMigrationContract(false) current := schemaFlagMigrationContract(true) normalized, err := normalizeSchemaFlagMigrations(baseline, current, schemaFlagMigrationAuthorizations()) if err != nil { t.Fatal(err) } if failures := checkCompatibility(normalized, current); len(failures) != 0 { t.Fatalf("exact flag migration should pass after normalization: %v", failures) } tool := normalized.Products["chat"].Tools["chat.edit_message"] for _, legacy := range []string{"group", "id", "msg-id"} { if _, exists := tool.Parameters[legacy]; exists { t.Fatalf("normalized baseline retained legacy parameter %q", legacy) } } if canonical := tool.Parameters["conversation-id"]; canonical.Required || canonical.CLIRequired { t.Fatalf("optional canonical rename changed requiredness: %#v", canonical) } if canonical := tool.Parameters["message-id"]; !canonical.Required || !canonical.CLIRequired { t.Fatalf("required canonical rename changed requiredness: %#v", canonical) } if tool.Constraints != current.Products["chat"].Tools["chat.edit_message"].Constraints { t.Fatalf("constraints were not normalized: %s", tool.Constraints) } } func TestCrossPlatformCoverageSchemaFlagMigrationRejectsSemanticDrift(t *testing.T) { tests := []struct { name string want string mutate func(*parameterSchema) }{ {name: "required decline", want: "must be required and cli_required", mutate: func(parameter *parameterSchema) { parameter.Required = false }}, {name: "cli required decline", want: "must be required and cli_required", mutate: func(parameter *parameterSchema) { parameter.CLIRequired = false }}, {name: "type", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Type = `"integer"` }}, {name: "property", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Property = "other" }}, {name: "interface type", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.InterfaceType = "integer" }}, {name: "required when", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.RequiredWhen = "mode=other" }}, {name: "default", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Default = `"other"` }}, {name: "interface default", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.InterfaceDefault = `"other"` }}, {name: "format", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Format = "uri" }}, {name: "enum", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Enum = []string{"a"} }}, {name: "enum same length", want: "non-migration field", mutate: func(parameter *parameterSchema) { parameter.Enum = []string{"a", "c"} }}, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { current := schemaFlagMigrationContract(true) product := current.Products["chat"] tool := product.Tools["chat.edit_message"] canonical := tool.Parameters["message-id"] test.mutate(&canonical) tool.Parameters["message-id"] = canonical product.Tools["chat.edit_message"] = tool current.Products["chat"] = product _, err := normalizeSchemaFlagMigrations( schemaFlagMigrationContract(false), current, schemaFlagMigrationAuthorizations(), ) if err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("normalizeSchemaFlagMigrations() error = %v, want %q", err, test.want) } }) } for _, test := range []struct { name string want string mutate func(*parameterSchema) }{ {name: "optional required promotion", want: "changed requiredness", mutate: func(parameter *parameterSchema) { parameter.Required = true }}, {name: "optional cli_required promotion", want: "changed cli_required", mutate: func(parameter *parameterSchema) { parameter.CLIRequired = true }}, } { t.Run(test.name, func(t *testing.T) { current := schemaFlagMigrationContract(true) product := current.Products["chat"] tool := product.Tools["chat.edit_message"] canonical := tool.Parameters["conversation-id"] test.mutate(&canonical) tool.Parameters["conversation-id"] = canonical product.Tools["chat.edit_message"] = tool current.Products["chat"] = product _, err := normalizeSchemaFlagMigrations( schemaFlagMigrationContract(false), current, schemaFlagMigrationAuthorizations(), ) if err == nil || !strings.Contains(err.Error(), test.want) { t.Fatalf("normalizeSchemaFlagMigrations() error = %v, want %q", err, test.want) } }) } } func TestCrossPlatformCoverageSchemaFlagMigrationAdapterBranches(t *testing.T) { baseline := schemaFlagMigrationContract(false) current := schemaFlagMigrationContract(true) noConstraintsBaseline := cloneContract(baseline) product := noConstraintsBaseline.Products["chat"] tool := product.Tools["chat.edit_message"] tool.Constraints = "" product.Tools["chat.edit_message"] = tool noConstraintsBaseline.Products["chat"] = product noConstraintsCurrent := cloneContract(current) product = noConstraintsCurrent.Products["chat"] tool = product.Tools["chat.edit_message"] tool.Constraints = "" product.Tools["chat.edit_message"] = tool noConstraintsCurrent.Products["chat"] = product if _, err := normalizeSchemaFlagMigrations(noConstraintsBaseline, noConstraintsCurrent, schemaFlagMigrationAuthorizations()); err != nil { t.Fatalf("unchanged empty constraints: %v", err) } cliOnly := schemaFlagMigrationAuthorizations()[:1] cliOnly[0].Command = "dws chat cli-only" normalized, err := normalizeSchemaFlagMigrations(baseline, current, cliOnly) if err != nil || !reflect.DeepEqual(normalized, baseline) { t.Fatalf("CLI-only migration = %#v, %v", normalized, err) } duplicate := cloneContract(baseline) product = duplicate.Products["chat"] product.Tools["chat.another"] = product.Tools["chat.edit_message"] duplicate.Products["chat"] = product duplicate.Products["chat2"] = productSchema{Tools: map[string]toolSchema{ "chat.third": product.Tools["chat.edit_message"], }} if matches := schemaToolsByPrimaryPath(duplicate, "chat message edit"); len(matches) != 3 || matches[0].productID != "chat" || matches[0].toolID != "chat.another" { t.Fatalf("sorted duplicate Schema matches = %#v", matches) } if _, err := normalizeSchemaFlagMigrations(duplicate, current, schemaFlagMigrationAuthorizations()[:1]); err == nil || !strings.Contains(err.Error(), "matches 3") { t.Fatalf("duplicate Schema path error = %v", err) } missingCurrent := cloneContract(current) delete(missingCurrent.Products, "chat") normalized, err = normalizeSchemaFlagMigrations(baseline, missingCurrent, schemaFlagMigrationAuthorizations()) if err != nil { t.Fatal(err) } if failures := checkCompatibility(normalized, missingCurrent); len(failures) == 0 { t.Fatal("missing candidate product was normalized away") } canonicalOnlyMigration := schemaFlagMigrationAuthorizations()[0] canonicalOnlyMigration.Legacy.Name = "legacy-not-published-in-schema" canonicalOnlyBaseline := schemaFlagMigrationContract(true) driftedCanonical := cloneContract(canonicalOnlyBaseline) product = driftedCanonical.Products["chat"] tool = product.Tools["chat.edit_message"] canonical := tool.Parameters["conversation-id"] canonical.Property = "different" tool.Parameters["conversation-id"] = canonical product.Tools["chat.edit_message"] = tool driftedCanonical.Products["chat"] = product normalized, err = normalizeSchemaFlagMigrations(canonicalOnlyBaseline, driftedCanonical, []interfacesnapshot.FlagMigration{canonicalOnlyMigration}) if err != nil { t.Fatal(err) } if failures := strings.Join(checkCompatibility(normalized, driftedCanonical), "\n"); !strings.Contains(failures, "changed property") { t.Fatalf("canonical-only Schema drift was hidden: %s", failures) } promotedCanonical := cloneContract(canonicalOnlyBaseline) product = promotedCanonical.Products["chat"] tool = product.Tools["chat.edit_message"] canonical = tool.Parameters["conversation-id"] canonical.Required = true canonical.CLIRequired = true tool.Parameters["conversation-id"] = canonical product.Tools["chat.edit_message"] = tool promotedCanonical.Products["chat"] = product normalized, err = normalizeSchemaFlagMigrations( canonicalOnlyBaseline, promotedCanonical, []interfacesnapshot.FlagMigration{canonicalOnlyMigration}, ) if err != nil { t.Fatal(err) } if failures := strings.Join(checkCompatibility(normalized, promotedCanonical), "\n"); !strings.Contains(failures, "newly required") || !strings.Contains(failures, "newly cli_required") { t.Fatalf("canonical-only required promotion was hidden: %s", failures) } canonicalConflictBaseline := schemaFlagMigrationContract(false) product = canonicalConflictBaseline.Products["chat"] tool = product.Tools["chat.edit_message"] canonical = tool.Parameters["conversation-id"] canonical.Property = "historicalCanonicalProperty" tool.Parameters["conversation-id"] = canonical product.Tools["chat.edit_message"] = tool canonicalConflictBaseline.Products["chat"] = product if _, err := normalizeSchemaFlagMigrations( canonicalConflictBaseline, schemaFlagMigrationContract(true), schemaFlagMigrationAuthorizations()[:1], ); err == nil || !strings.Contains(err.Error(), "non-migration field") { t.Fatalf("historical canonical drift error = %v", err) } conflicting := schemaFlagMigrationAuthorizations()[0] conflicting.Canonical.Name = "other-conversation-id" conflicting.Legacy.After.AliasOf = conflicting.Canonical.Name product = current.Products["chat"] tool = product.Tools["chat.edit_message"] tool.Parameters[conflicting.Canonical.Name] = tool.Parameters["conversation-id"] product.Tools["chat.edit_message"] = tool current.Products["chat"] = product if _, err := normalizeSchemaFlagMigrations(baseline, current, []interfacesnapshot.FlagMigration{ schemaFlagMigrationAuthorizations()[0], conflicting, }); err == nil || !strings.Contains(err.Error(), "maps Schema parameter") { t.Fatalf("conflicting Schema mapping error = %v", err) } old := parameterSchema{Required: true, CLIRequired: true} if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], old, parameterSchema{CLIRequired: true}); err == nil || !strings.Contains(err.Error(), "changed requiredness") { t.Fatalf("direct required decline error = %v", err) } if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], old, parameterSchema{Required: true}); err == nil || !strings.Contains(err.Error(), "changed cli_required") { t.Fatalf("direct cli_required decline error = %v", err) } optional := parameterSchema{} if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], optional, parameterSchema{Required: true}); err == nil || !strings.Contains(err.Error(), "changed requiredness") { t.Fatalf("direct required promotion error = %v", err) } if err := validateRenamedSchemaParameter(schemaFlagMigrationAuthorizations()[0], optional, parameterSchema{CLIRequired: true}); err == nil || !strings.Contains(err.Error(), "changed cli_required") { t.Fatalf("direct cli_required promotion error = %v", err) } } func TestCrossPlatformCoverageSchemaFlagMigrationRejectsPartialAndUnrelatedChanges(t *testing.T) { baseline := schemaFlagMigrationContract(false) migrations := schemaFlagMigrationAuthorizations() current := schemaFlagMigrationContract(true) product := current.Products["chat"] tool := product.Tools["chat.edit_message"] delete(tool.Parameters, "message-id") product.Tools["chat.edit_message"] = tool current.Products["chat"] = product if _, err := normalizeSchemaFlagMigrations(baseline, current, migrations); err == nil || !strings.Contains(err.Error(), "does not publish canonical") { t.Fatalf("missing canonical error = %v", err) } current = schemaFlagMigrationContract(true) product = current.Products["chat"] tool = product.Tools["chat.edit_message"] tool.Parameters["msg-id"] = schemaFlagMigrationContract(false).Products["chat"].Tools["chat.edit_message"].Parameters["msg-id"] product.Tools["chat.edit_message"] = tool current.Products["chat"] = product if _, err := normalizeSchemaFlagMigrations(baseline, current, migrations); err == nil || !strings.Contains(err.Error(), "still publishes legacy") { t.Fatalf("published legacy error = %v", err) } current = schemaFlagMigrationContract(true) product = current.Products["chat"] tool = product.Tools["chat.edit_message"] delete(tool.Parameters, "unrelated") product.Tools["chat.edit_message"] = tool current.Products["chat"] = product normalized, err := normalizeSchemaFlagMigrations(baseline, current, migrations) if err != nil { t.Fatal(err) } if failures := strings.Join(checkCompatibility(normalized, current), "\n"); !strings.Contains(failures, `lost parameter "unrelated"`) { t.Fatalf("unrelated parameter removal was hidden: %s", failures) } current = schemaFlagMigrationContract(true) product = current.Products["chat"] tool = product.Tools["chat.edit_message"] tool.Constraints = `{"require_one_of":[["conversation-id"]],"require_together":[["conversation-id","unrelated"]]}` product.Tools["chat.edit_message"] = tool current.Products["chat"] = product normalized, err = normalizeSchemaFlagMigrations(baseline, current, migrations) if err != nil { t.Fatal(err) } if failures := strings.Join(checkCompatibility(normalized, current), "\n"); !strings.Contains(failures, "changed constraints") { t.Fatalf("unrelated constraint change was hidden: %s", failures) } // A CLI ledger entry alone cannot authorize a Schema constraint rewrite // when the historical Schema published neither the legacy nor canonical // parameter involved in that entry. noSchemaEvidence := schemaFlagMigrationContract(false) product = noSchemaEvidence.Products["chat"] tool = product.Tools["chat.edit_message"] for _, name := range []string{"conversation-id", "group", "id", "msg-id"} { delete(tool.Parameters, name) } product.Tools["chat.edit_message"] = tool noSchemaEvidence.Products["chat"] = product normalized, err = normalizeSchemaFlagMigrations(noSchemaEvidence, schemaFlagMigrationContract(true), migrations) if err != nil { t.Fatal(err) } if failures := strings.Join(checkCompatibility(normalized, schemaFlagMigrationContract(true)), "\n"); !strings.Contains(failures, "changed constraints") { t.Fatalf("constraint rewrite without Schema parameter evidence was hidden: %s", failures) } // A baseline that already contains only the canonical parameter is not // evidence that a stray legacy name in constraints belongs to the migration. canonicalOnly := schemaFlagMigrationContract(true) product = canonicalOnly.Products["chat"] tool = product.Tools["chat.edit_message"] tool.Constraints = `{"require_one_of":[["group"]]}` product.Tools["chat.edit_message"] = tool canonicalOnly.Products["chat"] = product normalized, err = normalizeSchemaFlagMigrations(canonicalOnly, schemaFlagMigrationContract(true), migrations) if err != nil { t.Fatal(err) } if failures := strings.Join(checkCompatibility(normalized, schemaFlagMigrationContract(true)), "\n"); !strings.Contains(failures, "changed constraints") { t.Fatalf("canonical-only baseline authorized a legacy constraint rewrite: %s", failures) } } func TestCrossPlatformCoverageSchemaFlagMigrationConstraintsFailClosed(t *testing.T) { for _, constraints := range []string{ `{`, `{"`, `{"require_one_of":`, `{"unknown_kind":[["conversation-id"]]}`, `{"require_one_of":"conversation-id"}`, `{"require_one_of":["conversation-id"]}`, `{"require_one_of":null}`, `{"require_one_of":[[]]}`, `{"require_one_of":[[" conversation-id"]]}`, `{"require_one_of":[["conversation-id"]],"require_one_of":[["conversation-id"]]}`, `{"require_one_of":[]`, `{} {}`, `[]`, } { current := schemaFlagMigrationContract(true) product := current.Products["chat"] tool := product.Tools["chat.edit_message"] tool.Constraints = constraints product.Tools["chat.edit_message"] = tool current.Products["chat"] = product normalized, err := normalizeSchemaFlagMigrations( schemaFlagMigrationContract(false), current, schemaFlagMigrationAuthorizations(), ) if err != nil { t.Fatal(err) } if failures := strings.Join(checkCompatibility(normalized, current), "\n"); !strings.Contains(failures, "changed constraints") { t.Fatalf("malformed constraints %s were authorized: %s", constraints, failures) } } if encoded, ok := canonicalizeMigratedConstraints("", nil); !ok || encoded != "" { t.Fatalf("empty migrated constraints = %q, %v", encoded, ok) } encoded, ok := canonicalizeMigratedConstraints( `{"require_one_of":[["b"],["a"],["a"],["c","c"]]}`, nil, ) if !ok || encoded != `{"require_one_of":[["a"],["b"],["c"]]}` { t.Fatalf("canonical migrated constraint groups = %q, %v", encoded, ok) } } func TestCrossPlatformCoverageSchemaMigrationCLIRequiresCompleteCheckInputs(t *testing.T) { allInputs := []string{ "--approved-flag-migrations", "approved.json", "--candidate-flag-migrations", "candidate.json", "--migration-current-snapshot", "current.json", "--migration-base-snapshot", "base.json", "--migration-stable-snapshot", "stable.json", } var stdout, stderr bytes.Buffer if code := run([]string{"--check", "baseline.json", "--current", "schema.json", "--approved-flag-migrations", "approved.json"}, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "both flag manifests") { t.Fatalf("partial migration inputs code=%d stderr=%q", code, stderr.String()) } for _, mode := range []string{"--normalize", "--merge"} { stderr.Reset() args := append([]string{mode, "schema.json"}, allInputs...) if code := run(args, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "only valid with --check") { t.Fatalf("%s migration inputs code=%d stderr=%q", mode, code, stderr.String()) } } } func TestCrossPlatformCoverageSchemaMigrationLifecycleAndRun(t *testing.T) { directory := t.TempDir() baselinePath := filepath.Join(directory, "schema-baseline.json") currentSchemaPath := filepath.Join(directory, "schema-current.json") writeSchemaContractFile(t, baselinePath, schemaFlagMigrationContract(false)) writeRawSchemaContractFile(t, currentSchemaPath, schemaFlagMigrationContract(true)) approvedPath := filepath.Join(directory, "approved.json") candidatePath := filepath.Join(directory, "candidate.json") currentSnapshotPath := filepath.Join(directory, "interface-current.json") baseSnapshotPath := filepath.Join(directory, "interface-base.json") stableSnapshotPath := filepath.Join(directory, "interface-stable.json") writeFlagMigrationManifestFile(t, approvedPath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationPending)) writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed)) writeInterfaceSnapshotFile(t, currentSnapshotPath, schemaFlagMigrationSnapshot(true)) writeInterfaceSnapshotFile(t, baseSnapshotPath, schemaFlagMigrationSnapshot(false)) writeInterfaceSnapshotFile(t, stableSnapshotPath, schemaFlagMigrationSnapshot(false)) args := []string{ "--check", baselinePath, "--current", currentSchemaPath, "--approved-flag-migrations", approvedPath, "--candidate-flag-migrations", candidatePath, "--migration-current-snapshot", currentSnapshotPath, "--migration-base-snapshot", baseSnapshotPath, "--migration-stable-snapshot", stableSnapshotPath, } var stdout, stderr bytes.Buffer if code := run(args, &stdout, &stderr); code != 0 { t.Fatalf("governed Schema check code=%d stderr=%s", code, stderr.String()) } if !strings.Contains(stdout.String(), "compatibility check: ok") { t.Fatalf("governed Schema check output = %q", stdout.String()) } badArgs := append([]string(nil), args...) badArgs[5] = filepath.Join(directory, "missing-approved.json") stderr.Reset() if code := run(badArgs, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "authorize Schema flag migrations") { t.Fatalf("authorization error code=%d stderr=%q", code, stderr.String()) } legacyCurrent := schemaFlagMigrationContract(true) product := legacyCurrent.Products["chat"] tool := product.Tools["chat.edit_message"] tool.Parameters["msg-id"] = schemaFlagMigrationContract(false).Products["chat"].Tools["chat.edit_message"].Parameters["msg-id"] product.Tools["chat.edit_message"] = tool legacyCurrent.Products["chat"] = product writeRawSchemaContractFile(t, currentSchemaPath, legacyCurrent) stderr.Reset() if code := run(args, &stdout, &stderr); code != 2 || !strings.Contains(stderr.String(), "normalize approved Schema flag migrations") { t.Fatalf("normalization error code=%d stderr=%q", code, stderr.String()) } // Candidate-owned pending records cannot approve their own after-state. writeFlagMigrationManifestFile(t, approvedPath, interfacesnapshot.FlagMigrationManifest{ Version: interfacesnapshot.FlagMigrationManifestVersion, Migrations: []interfacesnapshot.FlagMigration{}, }) writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationPending)) if _, err := authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath); err == nil || !strings.Contains(err.Error(), "cannot authorize its own interface change") { t.Fatalf("self-approval error = %v", err) } // An incomplete after-state fails lifecycle authorization before Schema is normalized. writeFlagMigrationManifestFile(t, approvedPath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationPending)) writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed)) partial := schemaFlagMigrationSnapshot(true) partial.Commands[0].LocalFlags = partial.Commands[0].LocalFlags[:len(partial.Commands[0].LocalFlags)-1] writeInterfaceSnapshotFile(t, currentSnapshotPath, partial) if _, err := authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath); err == nil || !strings.Contains(err.Error(), "partially applied") { t.Fatalf("partial migration error = %v", err) } } func TestCrossPlatformCoverageSchemaMigrationFileErrors(t *testing.T) { directory := t.TempDir() approvedPath := filepath.Join(directory, "approved.json") candidatePath := filepath.Join(directory, "candidate.json") currentPath := filepath.Join(directory, "current.json") basePath := filepath.Join(directory, "base.json") stablePath := filepath.Join(directory, "stable.json") writeFlagMigrationManifestFile(t, approvedPath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationPending)) writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed)) writeInterfaceSnapshotFile(t, currentPath, schemaFlagMigrationSnapshot(true)) writeInterfaceSnapshotFile(t, basePath, schemaFlagMigrationSnapshot(false)) writeInterfaceSnapshotFile(t, stablePath, schemaFlagMigrationSnapshot(false)) paths := []string{approvedPath, candidatePath, currentPath, basePath, stablePath} wants := []string{ "read approved flag migrations", "read candidate flag migrations", "read migration current snapshot", "read migration base snapshot", "read migration stable snapshot", } for index, want := range wants { t.Run(want, func(t *testing.T) { invalid := append([]string(nil), paths...) invalid[index] = filepath.Join(directory, "missing.json") _, err := authorizeSchemaFlagMigrations(invalid[0], invalid[1], invalid[2], invalid[3], invalid[4]) if err == nil || !strings.Contains(err.Error(), want) { t.Fatalf("authorizeSchemaFlagMigrations() error = %v, want %q", err, want) } }) } } func TestCrossPlatformCoverageSchemaConsumedReceiptIsNoOpForAfterBaseline(t *testing.T) { directory := t.TempDir() approvedPath := filepath.Join(directory, "approved.json") candidatePath := filepath.Join(directory, "candidate.json") currentSnapshotPath := filepath.Join(directory, "current.json") baseSnapshotPath := filepath.Join(directory, "base.json") stableSnapshotPath := filepath.Join(directory, "stable.json") writeFlagMigrationManifestFile(t, approvedPath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed)) writeFlagMigrationManifestFile(t, candidatePath, schemaFlagMigrationManifest(interfacesnapshot.FlagMigrationConsumed)) writeInterfaceSnapshotFile(t, currentSnapshotPath, schemaFlagMigrationSnapshot(true)) writeInterfaceSnapshotFile(t, baseSnapshotPath, schemaFlagMigrationSnapshot(true)) writeInterfaceSnapshotFile(t, stableSnapshotPath, schemaFlagMigrationSnapshot(false)) migrations, err := authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath) if err != nil { t.Fatal(err) } baseline := schemaFlagMigrationContract(true) normalized, err := normalizeSchemaFlagMigrations(baseline, schemaFlagMigrationContract(true), migrations) if err != nil { t.Fatal(err) } if !reflect.DeepEqual(normalized, baseline) { t.Fatalf("consumed receipt changed an already-after Schema baseline: %#v", normalized) } if failures := checkCompatibility(normalized, schemaFlagMigrationContract(true)); len(failures) != 0 { t.Fatalf("consumed receipt did not preserve compatibility: %v", failures) } // Once stable also reaches after, retaining the consumed receipt is stale; // deleting it yields no authorization and the already-after Schema passes. writeInterfaceSnapshotFile(t, stableSnapshotPath, schemaFlagMigrationSnapshot(true)) if _, err := authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath); err == nil || !strings.Contains(err.Error(), "stale after all references reached the after state") { t.Fatalf("stale consumed receipt error = %v", err) } emptyManifest := interfacesnapshot.FlagMigrationManifest{ Version: interfacesnapshot.FlagMigrationManifestVersion, Migrations: []interfacesnapshot.FlagMigration{}, } writeFlagMigrationManifestFile(t, candidatePath, emptyManifest) migrations, err = authorizeSchemaFlagMigrations(approvedPath, candidatePath, currentSnapshotPath, baseSnapshotPath, stableSnapshotPath) if err != nil || len(migrations) != 0 { t.Fatalf("cleaned consumed receipt authorizations = %#v, %v", migrations, err) } baselinePath := filepath.Join(directory, "schema-baseline.json") currentSchemaPath := filepath.Join(directory, "schema-current.json") writeSchemaContractFile(t, baselinePath, baseline) writeRawSchemaContractFile(t, currentSchemaPath, schemaFlagMigrationContract(true)) args := []string{ "--check", baselinePath, "--current", currentSchemaPath, "--approved-flag-migrations", approvedPath, "--candidate-flag-migrations", candidatePath, "--migration-current-snapshot", currentSnapshotPath, "--migration-base-snapshot", baseSnapshotPath, "--migration-stable-snapshot", stableSnapshotPath, } var stdout, stderr bytes.Buffer if code := run(args, &stdout, &stderr); code != 0 { t.Fatalf("cleaned consumed receipt check code=%d stderr=%q", code, stderr.String()) } } func schemaFlagMigrationContract(after bool) schemaContract { legacyMessage := parameterSchema{ Type: `"string"`, Property: "openMessageId", InterfaceType: "string", Required: true, CLIRequired: true, Default: `"default-message"`, InterfaceDefault: `"default-message"`, Format: "id", Enum: []string{"a", "b"}, } conversation := parameterSchema{ Type: `"string"`, Property: "openConversationId", InterfaceType: "string", } parameters := map[string]parameterSchema{ "unrelated": {Type: `"string"`, Property: "unrelated"}, } constraints := `{"require_one_of":[["group","id"]]}` if after { parameters["conversation-id"] = conversation parameters["message-id"] = legacyMessage constraints = `{"require_one_of":[["conversation-id"]]}` } else { parameters["group"] = conversation parameters["id"] = conversation parameters["msg-id"] = legacyMessage } return schemaContract{Version: schemaContractVersion, Products: map[string]productSchema{ "chat": {Tools: map[string]toolSchema{ "chat.edit_message": { PrimaryCLIPath: "chat message edit", InterfaceMode: "mcp", Availability: "available", Parameters: parameters, Constraints: constraints, Effect: "write", Risk: "medium", Confirmation: "not_required", Idempotency: "unknown", }, }}, }} } func schemaFlagMigrationAuthorizations() []interfacesnapshot.FlagMigration { conversationBefore := interfacesnapshot.FlagMigrationState{ Present: true, Type: "string", Scope: "local", } conversationAfter := conversationBefore messageBefore := interfacesnapshot.FlagMigrationState{ Present: true, Type: "string", Required: true, Scope: "local", } messageAfter := messageBefore return []interfacesnapshot.FlagMigration{ { Command: "dws chat message edit", Legacy: interfacesnapshot.FlagMigrationSide{ Name: "group", Before: conversationBefore, After: interfacesnapshot.FlagMigrationState{ Present: true, Type: "string", Hidden: true, Scope: "local", AliasOf: "conversation-id", }, }, Canonical: interfacesnapshot.FlagMigrationSide{ Name: "conversation-id", Before: interfacesnapshot.FlagMigrationState{}, After: conversationAfter, }, }, { Command: "dws chat message edit", Legacy: interfacesnapshot.FlagMigrationSide{ Name: "id", Before: conversationBefore, After: interfacesnapshot.FlagMigrationState{ Present: true, Type: "string", Hidden: true, Scope: "local", AliasOf: "conversation-id", }, }, Canonical: interfacesnapshot.FlagMigrationSide{ Name: "conversation-id", Before: interfacesnapshot.FlagMigrationState{}, After: conversationAfter, }, }, { Command: "dws chat message edit", Legacy: interfacesnapshot.FlagMigrationSide{ Name: "msg-id", Before: messageBefore, After: interfacesnapshot.FlagMigrationState{ Present: true, Type: "string", Hidden: true, Scope: "local", AliasOf: "message-id", }, }, Canonical: interfacesnapshot.FlagMigrationSide{ Name: "message-id", Before: interfacesnapshot.FlagMigrationState{}, After: messageAfter, }, }, } } func schemaFlagMigrationManifest(state string) interfacesnapshot.FlagMigrationManifest { migrations := schemaFlagMigrationAuthorizations() for index := range migrations { migrations[index].State = state migrations[index].Reason = "canonicalize historical public flag names" } return interfacesnapshot.FlagMigrationManifest{ Version: interfacesnapshot.FlagMigrationManifestVersion, Migrations: migrations, } } func schemaFlagMigrationSnapshot(after bool) interfacesnapshot.Snapshot { states := map[string]interfacesnapshot.FlagMigrationState{} for _, migration := range schemaFlagMigrationAuthorizations() { if after { states[migration.Legacy.Name] = migration.Legacy.After states[migration.Canonical.Name] = migration.Canonical.After } else { states[migration.Legacy.Name] = migration.Legacy.Before states[migration.Canonical.Name] = migration.Canonical.Before } } names := make([]string, 0, len(states)) for name, state := range states { if state.Present { names = append(names, name) } } sort.Strings(names) flags := make([]interfacesnapshot.Flag, 0, len(names)) for _, name := range names { state := states[name] flags = append(flags, interfacesnapshot.Flag{ Name: name, Type: state.Type, Required: state.Required, Hidden: state.Hidden, Shorthand: state.Shorthand, NoOpt: state.NoOpt, AliasOf: state.AliasOf, }) } return interfacesnapshot.Snapshot{ SchemaVersion: interfacesnapshot.SchemaVersion, Rules: interfacesnapshot.Rules{ ExcludedCommandSubtrees: []string{}, ExcludedFlags: []string{}, }, Commands: []interfacesnapshot.Command{{ Path: "dws chat message edit", Runnable: true, Aliases: []string{}, LocalFlags: flags, }}, } } func writeFlagMigrationManifestFile(t *testing.T, path string, manifest interfacesnapshot.FlagMigrationManifest) { t.Helper() data, err := json.MarshalIndent(manifest, "", " ") if err != nil { t.Fatal(err) } if err := os.WriteFile(path, append(data, '\n'), 0o600); err != nil { t.Fatal(err) } } func writeInterfaceSnapshotFile(t *testing.T, path string, snapshot interfacesnapshot.Snapshot) { t.Helper() var encoded bytes.Buffer if err := interfacesnapshot.Write(&encoded, snapshot); err != nil { t.Fatal(err) } if err := os.WriteFile(path, encoded.Bytes(), 0o600); err != nil { t.Fatal(err) } } func writeSchemaContractFile(t *testing.T, path string, contract schemaContract) { t.Helper() var encoded bytes.Buffer if err := writeContract(&encoded, contract); err != nil { t.Fatal(err) } if err := os.WriteFile(path, encoded.Bytes(), 0o600); err != nil { t.Fatal(err) } } func writeRawSchemaContractFile(t *testing.T, path string, contract schemaContract) { t.Helper() products := make([]map[string]any, 0, len(contract.Products)) for productID, product := range contract.Products { tools := make([]map[string]any, 0, len(product.Tools)) for toolID, tool := range product.Tools { parameters := map[string]any{} for name, parameter := range tool.Parameters { raw := map[string]any{ "type": json.RawMessage(parameter.Type), "required": parameter.Required, "cli_required": parameter.CLIRequired, "property": parameter.Property, "interface_type": parameter.InterfaceType, "required_when": parameter.RequiredWhen, "format": parameter.Format, "enum": parameter.Enum, "field_provenance": map[string]any{}, } if parameter.Default != "" { raw["default"] = json.RawMessage(parameter.Default) } if parameter.InterfaceDefault != "" { raw["interface_default"] = json.RawMessage(parameter.InterfaceDefault) } parameters[name] = raw } rawTool := map[string]any{ "canonical_path": toolID, "primary_cli_path": tool.PrimaryCLIPath, "interface_mode": tool.InterfaceMode, "availability": tool.Availability, "parameters": parameters, "effect": tool.Effect, "risk": tool.Risk, "confirmation": tool.Confirmation, "idempotency": tool.Idempotency, "field_provenance": map[string]any{}, } if tool.InterfaceRef != "" { rawTool["interface_ref"] = json.RawMessage(tool.InterfaceRef) } if tool.Constraints != "" { rawTool["constraints"] = json.RawMessage(tool.Constraints) } if len(tool.Positionals) > 0 { rawTool["positionals"] = tool.Positionals } if tool.DryRun != "" { rawTool["dry_run"] = json.RawMessage(tool.DryRun) } tools = append(tools, rawTool) } products = append(products, map[string]any{"id": productID, "tools": tools}) } payload := map[string]any{"kind": "schema", "products": products} data, err := json.Marshal(payload) if err != nil { t.Fatal(err) } if err := os.WriteFile(path, data, 0o600); err != nil { t.Fatal(err) } }