package contractgovernance import ( "io/fs" "os" "path/filepath" "regexp" "runtime" "strings" "testing" ) func TestDeviceContractCompatibilityGuardrails(t *testing.T) { root := repositoryRoot(t) contract := readFile(t, filepath.Join(root, "openapi", "openapi.yaml")) versions := regexp.MustCompile(`(?m)^ /device/v([0-9]+)(?:/[^:]+)?:$`).FindAllStringSubmatch(contract, -1) if len(versions) < 10 { t.Fatalf("found only %d versioned device paths; contract scan likely stopped matching", len(versions)) } v2Count, v3Count := 0, 0 for _, match := range versions { switch match[1] { case "2": v2Count++ case "3": v3Count++ default: t.Fatalf("device path %q uses an unsupported compatibility generation", match[0]) } } if v2Count < 10 || v3Count < 2 { t.Fatalf("device contract must preserve v2 and publish hardware-agent v3; v2=%d v3=%d", v2Count, v3Count) } for _, required := range []string{ "rtc_credential_renewal:", "省略时保持旧版v2终端行为", "设备不得据此改变采集、编码、发布、订阅或播放逻辑", } { if !strings.Contains(contract, required) { t.Fatalf("OpenAPI is missing device compatibility rule %q", required) } } contractVersion := regexp.MustCompile(`(?m)^ version: ([0-9]+\.[0-9]+\.[0-9]+)$`).FindStringSubmatch(contract) if len(contractVersion) != 2 { t.Fatal("OpenAPI info.version must be a semantic version") } notes := readFile(t, filepath.Join(root, "docs", "device-api-compatibility.md")) if !strings.Contains(notes, "当前接口契约:OpenAPI "+contractVersion[1]) || !strings.Contains(notes, "/device/v2") || !strings.Contains(notes, "/device/v3") { t.Fatal("device compatibility notes are not synchronized with the current contract and next breaking version") } } func TestDeviceBehaviorDoesNotDependOnUserAgentOrEngineMode(t *testing.T) { root := repositoryRoot(t) for _, relative := range []string{"internal/deviceauth", "internal/httpapi", "internal/orchestration", "cmd/sim-device"} { directory := filepath.Join(root, filepath.FromSlash(relative)) err := filepath.WalkDir(directory, func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") { return nil } contents := readFile(t, path) for _, forbidden := range []string{`Header.Get("User-Agent")`, ".UserAgent()"} { if strings.Contains(contents, forbidden) { t.Errorf("%s uses firmware User-Agent for behavior selection", filepath.ToSlash(path)) } } if relative == "cmd/sim-device" && strings.Contains(contents, "engine_mode") { t.Errorf("%s makes the terminal aware of diagnostic-only engine_mode", filepath.ToSlash(path)) } return nil }) if err != nil { t.Fatalf("scan %s: %v", relative, err) } } } func repositoryRoot(t *testing.T) string { t.Helper() _, current, _, ok := runtime.Caller(0) if !ok { t.Fatal("resolve contract test location") } return filepath.Clean(filepath.Join(filepath.Dir(current), "..", "..")) } func readFile(t *testing.T, path string) string { t.Helper() contents, err := os.ReadFile(path) if err != nil { t.Fatalf("read %s: %v", path, err) } return string(contents) }