package environment import ( "os" "os/exec" "path/filepath" "strings" "testing" ) // 迁移文件一旦按版本号记录进 schema_migration 就不会再执行。 // 内容修正是用版本号不变的方式提交的话,已 applied 的库永远拿不到修正, // 造成不同库 schema 漂移(034 曾因此漏删 browser_env.image_version, // 触发 "environment persistence operation failed")。 // 本测试锁死规则:迁移文件内容自引入起不可变更;要改就新增版本号。 func TestMigrationFilesAreImmutableAfterIntroduction(t *testing.T) { // 迁移文件相对本包的路径 → 仓库根相对路径(git show 需要仓库根路径)。 repoPath := map[string]string{ "migrations/*.sql": "internal/environment", "../creator/migrations/*.sql": "internal/creator", "../account/migrations/*.sql": "internal/account", } for pattern, repoDir := range repoPath { entries, err := filepath.Glob(pattern) if err != nil { t.Fatalf("list migrations for %s: %v", pattern, err) } for _, file := range entries { // repoDir 已是仓库根相对包目录,glob 出的文件按 basename 拼回。/migrations/.sql file, repoFile := file, repoDir+"/migrations/"+filepath.Base(file) t.Run(file, func(t *testing.T) { introduced := gitOutput(t, "log", "--format=%H", "--diff-filter=A", "--", repoFile) if introduced == "" { t.Skipf("no introducing commit found for %s", repoFile) } firstCommit := strings.SplitN(introduced, "\n", 2)[0] original := gitOutput(t, "show", firstCommit+":"+repoFile) current, err := os.ReadFile(file) if err != nil { t.Fatalf("read %s: %v", file, err) } if original != string(current) { t.Errorf("migration %s was modified after introduction in %s; migrations are immutable — add a new versioned migration instead", repoFile, firstCommit) } }) } } } func gitOutput(t *testing.T, args ...string) string { t.Helper() cmd := exec.Command("git", args...) cmd.Dir = repoRoot(t) out, err := cmd.Output() if err != nil { t.Skipf("git %s unavailable: %v", args[0], err) } return string(out) } // repoRoot 定位 git 仓库根,保证 glob 出的包相对路径都能转成仓库根相对路径。 func repoRoot(t *testing.T) string { t.Helper() out, err := exec.Command("git", "rev-parse", "--show-toplevel").Output() if err != nil { t.Skipf("git repository unavailable: %v", err) } return strings.TrimSpace(string(out)) }