Files
byre/internal/store/migrate_internal_test.go
T

179 lines
4.9 KiB
Go

package store
import (
"database/sql"
"path/filepath"
"slices"
"testing"
)
func openTestDB(t *testing.T, path string) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite", "file:"+path+"?_pragma=foreign_keys(1)&_txlock=immediate")
if err != nil {
t.Fatalf("open db: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}
func appliedNames(t *testing.T, db *sql.DB) []string {
t.Helper()
rows, err := db.Query(`SELECT name FROM migrations ORDER BY rowid`)
if err != nil {
t.Fatalf("list migrations: %v", err)
}
defer rows.Close()
names := []string{}
for rows.Next() {
name := ""
err = rows.Scan(&name)
if err != nil {
t.Fatalf("scan migration: %v", err)
}
names = append(names, name)
}
if rows.Err() != nil {
t.Fatalf("list migrations: %v", rows.Err())
}
return names
}
func migrationNames(ms []migration) []string {
names := []string{}
for _, m := range ms {
names = append(names, m.name)
}
return names
}
func insertReport(t *testing.T, db *sql.DB, id, payload string) {
t.Helper()
_, err := db.Exec(`
INSERT OR IGNORE INTO users (login, display_name, created_at) VALUES ('alice', 'Alice', 'x');
INSERT OR IGNORE INTO projects (id, name, repo_path, default_branch, remote, created_by, created_at) VALUES ('p1', 'demo', '/nonexistent', 'main', 'none', 'alice', 'x');
INSERT OR IGNORE INTO tasks (id, project_id, title, branch, worktree_path, base_ref, status, created_by, created_at) VALUES ('t1', 'p1', 't1', 't1', '/nonexistent', 'x', 'active', 'alice', 'x');
INSERT OR IGNORE INTO agent_sessions (id, task_id, vendor, model, vendor_session_id, state, role, created_by, created_at) VALUES ('s1', 't1', 'claude', '', 's1', 'idle', 'implementer', 'alice', 'x');
`)
if err != nil {
t.Fatalf("insert parents: %v", err)
}
_, err = db.Exec(`
INSERT INTO inbox_items (id, project_id, task_id, agent_session_id, thread_id, kind, priority, blocking, payload, state, created_at)
VALUES (?, 'p1', 't1', 's1', ?, 'report', 0, 0, ?, 'open', 'x')
`, id, id, payload)
if err != nil {
t.Fatalf("insert report: %v", err)
}
}
func reportPayload(t *testing.T, db *sql.DB, id string) string {
t.Helper()
payload := ""
err := db.QueryRow(`SELECT payload FROM inbox_items WHERE id = ?`, id).Scan(&payload)
if err != nil {
t.Fatalf("read report: %v", err)
}
return payload
}
func TestMigrateFreshDatabase(t *testing.T) {
db := openTestDB(t, filepath.Join(t.TempDir(), "byre.db"))
err := migrate(db, migrations)
if err != nil {
t.Fatalf("migrate: %v", err)
}
err = migrate(db, migrations)
if err != nil {
t.Fatalf("migrate again: %v", err)
}
got := appliedNames(t, db)
want := migrationNames(migrations)
if !slices.Equal(got, want) {
t.Fatalf("applied %v, want %v", got, want)
}
}
func TestMigrateFromUserVersion(t *testing.T) {
path := filepath.Join(t.TempDir(), "byre.db")
db := openTestDB(t, path)
for _, m := range migrations[:9] {
_, err := db.Exec(m.sql)
if err != nil {
t.Fatalf("old migration %s: %v", m.name, err)
}
}
_, err := db.Exec(`PRAGMA user_version = 9`)
if err != nil {
t.Fatalf("set user_version: %v", err)
}
insertReport(t, db, "r1", `{"text":"- shipped"}`)
db.Close()
db = openTestDB(t, path)
err = migrate(db, migrations)
if err != nil {
t.Fatalf("migrate: %v", err)
}
got := appliedNames(t, db)
want := migrationNames(migrations)
if !slices.Equal(got, want) {
t.Fatalf("applied %v, want %v", got, want)
}
payload := reportPayload(t, db, "r1")
if payload != `{"text":"- shipped"}` {
t.Fatalf("report payload %s, want it unchanged", payload)
}
}
func TestMigrateRunsMigrationAddedBeforeApplied(t *testing.T) {
db := openTestDB(t, filepath.Join(t.TempDir(), "byre.db"))
first := migration{"create-a", `CREATE TABLE a (x TEXT)`}
other := migration{"create-b", `CREATE TABLE b (x TEXT)`}
last := migration{"create-c", `CREATE TABLE c (x TEXT)`}
err := migrate(db, []migration{first, last})
if err != nil {
t.Fatalf("migrate: %v", err)
}
err = migrate(db, []migration{first, other, last})
if err != nil {
t.Fatalf("migrate with other branch: %v", err)
}
got := appliedNames(t, db)
want := []string{"create-a", "create-c", "create-b"}
if !slices.Equal(got, want) {
t.Fatalf("applied %v, want %v", got, want)
}
_, err = db.Exec(`INSERT INTO b (x) VALUES ('y')`)
if err != nil {
t.Fatalf("table from other branch: %v", err)
}
}
func TestFlattenReportPayloadsRunsTwice(t *testing.T) {
db := openTestDB(t, filepath.Join(t.TempDir(), "byre.db"))
err := migrate(db, migrations)
if err != nil {
t.Fatalf("migrate: %v", err)
}
insertReport(t, db, "r1", `{"done":["one","two"]}`)
i := slices.IndexFunc(migrations, func(m migration) bool { return m.name == "flatten-report-payloads-to-text" })
for range 2 {
_, err = db.Exec(migrations[i].sql)
if err != nil {
t.Fatalf("flatten: %v", err)
}
payload := reportPayload(t, db, "r1")
if payload != `{"text":"- one\n- two"}` {
t.Fatalf("report payload %s", payload)
}
}
}