217 lines
6.1 KiB
Go
217 lines
6.1 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 TestBlobsMigrationOverPositionalCopy(t *testing.T) {
|
|
db := openTestDB(t, filepath.Join(t.TempDir(), "byre.db"))
|
|
err := migrate(db, migrations[:9])
|
|
if err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
insertReport(t, db, "r1", `{"text":"x"}`)
|
|
_, err = db.Exec(`
|
|
CREATE TABLE blobs (id TEXT PRIMARY KEY, content_type TEXT NOT NULL, path TEXT NOT NULL, created_by TEXT NOT NULL REFERENCES users(login), created_at TEXT NOT NULL);
|
|
INSERT INTO blobs VALUES ('b1', 'image/png', 'blobs/b1', 'alice', 'x');
|
|
ALTER TABLE turn_queue ADD COLUMN images TEXT NOT NULL DEFAULT '[]';
|
|
INSERT INTO turn_queue (session_id, actor, text, created_at) VALUES ('s1', 'alice', 'queued', 'x');
|
|
`)
|
|
if err != nil {
|
|
t.Fatalf("positional copy: %v", err)
|
|
}
|
|
|
|
err = migrate(db, migrations)
|
|
if err != nil {
|
|
t.Fatalf("migrate: %v", err)
|
|
}
|
|
|
|
text := ""
|
|
images := ""
|
|
err = db.QueryRow(`SELECT text, images FROM turn_queue WHERE session_id = 's1'`).Scan(&text, &images)
|
|
if err != nil {
|
|
t.Fatalf("queued turn: %v", err)
|
|
}
|
|
if text != "queued" || images != "[]" {
|
|
t.Fatalf("queued turn %q %q", text, images)
|
|
}
|
|
blobs := 0
|
|
err = db.QueryRow(`SELECT count(*) FROM blobs`).Scan(&blobs)
|
|
if err != nil || blobs != 1 {
|
|
t.Fatalf("blobs %d: %v", blobs, 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)
|
|
}
|
|
}
|
|
}
|