Files
chorus/migrations/migrations_test.go
T

120 lines
4.2 KiB
Go

package migrations
import (
"os"
"path/filepath"
"regexp"
"slices"
"strings"
"testing"
)
func TestMigrationPairsAndProductionTables(t *testing.T) {
upFiles, err := filepath.Glob("*.up.sql")
if err != nil {
t.Fatal(err)
}
downFiles, err := filepath.Glob("*.down.sql")
if err != nil {
t.Fatal(err)
}
if len(upFiles) == 0 || len(upFiles) != len(downFiles) {
t.Fatalf("migration pairs mismatch: up=%d down=%d", len(upFiles), len(downFiles))
}
wantFiles := []string{
"000001_mvp0_base.up.sql",
"000002_admin_baseline.up.sql",
"000003_mvp1_routing.up.sql",
"000004_mvp1_admin_seed.up.sql",
}
slices.Sort(upFiles)
if !slices.Equal(upFiles, wantFiles) {
t.Fatalf("up migrations = %v, want %v", upFiles, wantFiles)
}
for _, upFile := range upFiles {
downFile := strings.TrimSuffix(upFile, ".up.sql") + ".down.sql"
if _, err := os.Stat(downFile); err != nil {
t.Errorf("%s has no matching down migration: %v", upFile, err)
}
}
content, err := os.ReadFile("000001_mvp0_base.up.sql")
if err != nil {
t.Fatal(err)
}
sql := string(content)
for _, table := range []string{"users", "providers", "provider_models", "prompt_templates", "generations", "generation_inputs", "generation_outputs"} {
pattern := regexp.MustCompile(`(?i)CREATE TABLE\s+` + regexp.QuoteMeta(table) + `\s*\(`)
if !pattern.MatchString(sql) {
t.Errorf("migration does not create %s", table)
}
}
for _, required := range []string{"rendered_prompt", "attempts", "error_code", "error_message", "lease_token", "uq_generations_user_idempotency", "idx_generations_queue"} {
if !strings.Contains(sql, required) {
t.Errorf("migration is missing %s", required)
}
}
}
func TestMVP1MigrationContracts(t *testing.T) {
read := func(name string) string {
t.Helper()
content, err := os.ReadFile(name)
if err != nil {
t.Fatal(err)
}
return string(content)
}
adminBaseline := read("000002_admin_baseline.up.sql")
for _, table := range []string{
"sys_user", "sys_role", "sys_menu", "sys_api", "sys_role_menu", "sys_menu_api_rule",
"sys_casbin_rule", "sys_tables", "sys_columns",
} {
if !regexp.MustCompile(`(?i)CREATE TABLE\s+` + regexp.QuoteMeta(table) + `\s*\(`).MatchString(adminBaseline) {
t.Errorf("admin baseline does not create %s", table)
}
}
if strings.Contains(strings.ToLower(adminBaseline), "automigrate") {
t.Error("admin baseline must not use AutoMigrate")
}
routing := read("000003_mvp1_routing.up.sql")
for _, required := range []string{
"provider_credentials", "provider_model_capabilities", "route_pools", "active_routes",
"route_pool_members", "route_member_runtime", "provider_connectivity_checks", "admin_audit_events",
"active_credential_id", "route_snapshot", "provider_attempt_count", "default_role_rule", "note",
"uq_provider_credentials_version", "uq_route_pools_slug", "uq_route_pool_members_model",
"idx_provider_connectivity_checks_model", "uq_admin_audit_events_request_action",
"DROP CHECK chk_providers_auth", "UPDATE providers SET api_key_enc = NULL",
} {
if !strings.Contains(routing, required) {
t.Errorf("MVP-1 routing migration is missing %s", required)
}
}
seed := read("000004_mvp1_admin_seed.up.sql")
for _, required := range []string{"chorus_operator", "chorus.providers.credential.rotate", "sys_role_menu", "sys_menu_api_rule", "sys_casbin_rule"} {
if !strings.Contains(seed, required) {
t.Errorf("admin seed is missing %s", required)
}
}
if strings.Contains(seed, "INSERT INTO sys_user") {
t.Error("admin seed must not create a known administrator account")
}
adminDown := read("000002_admin_baseline.down.sql")
for _, required := range []string{"chorus_admin_baseline_down_guard", "INSERT INTO chorus_admin_baseline_down_guard", "EXISTS (SELECT 1 FROM sys_user)"} {
if !strings.Contains(adminDown, required) {
t.Errorf("admin baseline down migration is missing rollback guard %s", required)
}
}
for _, name := range []string{"000002_admin_baseline.down.sql", "000003_mvp1_routing.down.sql", "000004_mvp1_admin_seed.down.sql"} {
down := read(name)
if strings.Contains(strings.ToLower(down), "delete from sys_menu where title") || strings.Contains(strings.ToLower(down), "delete from sys_api where title") {
t.Errorf("%s must delete Chorus seed data by stable keys, not titles", name)
}
}
}