- fresh database, legacy upgrade (rows get one-shot access key, online=false), repeat startup idempotence - 1058 revised before any database applied it: drop dead token column, provision access_key via temporary default so non-empty legacy tables upgrade - immutable-migration rule skips PR-local migrations until merged; after merge the lock applies again
75 lines
2.8 KiB
Go
75 lines
2.8 KiB
Go
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 拼回。<pkg>/migrations/<name>.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]
|
||
// 引入 commit 尚未进入主线的迁移(同 PR 内迭代)允许修正内容;
|
||
// 合入主线后此跳过失效,规则恢复锁死。
|
||
if gitOutput(t, "merge-base", "--is-ancestor", firstCommit, "origin/main") == "" {
|
||
t.Skipf("%s introduced in %s which is not merged into origin/main; PR-local migrations may still be revised", repoFile, firstCommit)
|
||
}
|
||
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))
|
||
}
|