import { describe, expect, test } from "bun:test" import { $ } from "bun" import { fileURLToPath } from "url" import { SqliteClient } from "@effect/sql-sqlite-bun" import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" import { Effect } from "effect" import { sql } from "drizzle-orm" import { DatabaseMigration } from "@opencode-ai/core/database/migration" import sessionUsageMigration from "@opencode-ai/core/database/migration/20260510033149_session_usage" import sessionMetadataMigration from "@opencode-ai/core/database/migration/20260511173437_session-metadata" import type { SqlClient as SqlClientService } from "effect/unstable/sql/SqlClient" const run = (effect: Effect.Effect) => Effect.runPromise( effect.pipe(Effect.provide(SqliteClient.layer({ filename: ":memory:", disableWAL: true })), Effect.scoped), ) const makeDb = EffectDrizzleSqlite.makeWithDefaults() describe("DatabaseMigration", () => { if (process.platform === "linux") { test("declared schema has no ungenerated migrations", async () => { const result = await $`bun ${fileURLToPath(new URL("../script/migration.ts", import.meta.url))} --check` .quiet() .nothrow() expect(result.exitCode, result.stderr.toString()).toBe(0) expect(result.stdout.toString()).toContain("No schema changes, nothing to migrate") }, 30_000) } test("applies tracked migrations to an empty database", async () => { await run( Effect.gen(function* () { const db = yield* makeDb yield* DatabaseMigration.apply(db) expect(yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`)).toEqual({ name: "session", }) expect(yield* db.get(sql`SELECT count(*) as count FROM migration`)).toEqual({ count: 21 }) }), ) }) test("runs session usage backfill in order with schema changes", async () => { await run( Effect.gen(function* () { const db = yield* makeDb yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, time_updated integer NOT NULL)`) yield* db.run(sql`CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL, data text NOT NULL)`) yield* db.run(sql`INSERT INTO session (id, time_updated) VALUES ('session_1', 1)`) yield* db.run( sql`INSERT INTO message (id, session_id, data) VALUES ('message_1', 'session_1', '{"role":"assistant","cost":1.25,"tokens":{"input":2,"output":3,"reasoning":4,"cache":{"read":5,"write":6}}}')`, ) yield* DatabaseMigration.applyOnly(db, [sessionUsageMigration]) expect( yield* db.get( sql`SELECT cost, tokens_input, tokens_output, tokens_reasoning, tokens_cache_read, tokens_cache_write FROM session WHERE id = 'session_1'`, ), ).toEqual({ cost: 1.25, tokens_input: 2, tokens_output: 3, tokens_reasoning: 4, tokens_cache_read: 5, tokens_cache_write: 6, }) }), ) }) test("imports existing drizzle migration state", async () => { await run( Effect.gen(function* () { const db = yield* makeDb yield* db.run( sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`, ) yield* db.run(sql` INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at) VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()}) `) yield* DatabaseMigration.applyOnly(db, []) expect(yield* db.get(sql`SELECT id FROM migration`)).toEqual({ id: "20260127222353_familiar_lady_ursula" }) }), ) }) test("does not replay a migrated session metadata column", async () => { await run( Effect.gen(function* () { const db = yield* makeDb yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, metadata text)`) yield* db.run( sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`, ) yield* db.run(sql` INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at) VALUES ('hash', 1, '20260511173437_session-metadata', ${new Date().toISOString()}) `) yield* DatabaseMigration.applyOnly(db, [sessionMetadataMigration]) expect(yield* db.all(sql`SELECT id FROM migration`)).toEqual([{ id: "20260511173437_session-metadata" }]) }), ) }) test("accepts the temporary replacement session metadata migration id", async () => { await run( Effect.gen(function* () { const db = yield* makeDb yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY, metadata text)`) yield* db.run(sql`CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`) yield* db.run(sql`INSERT INTO migration (id, time_completed) VALUES ('20260530232709_lovely_romulus', 1)`) yield* DatabaseMigration.applyOnly(db, [sessionMetadataMigration]) expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual([ { id: "20260511173437_session-metadata" }, { id: "20260530232709_lovely_romulus" }, ]) }), ) }) test("skips drizzle import when migration table already has state", async () => { await run( Effect.gen(function* () { const db = yield* makeDb yield* db.run(sql`CREATE TABLE migration (id TEXT PRIMARY KEY, time_completed INTEGER NOT NULL)`) yield* db.run(sql`INSERT INTO migration (id, time_completed) VALUES ('existing', 1)`) yield* db.run( sql`CREATE TABLE __drizzle_migrations (id INTEGER PRIMARY KEY, hash text NOT NULL, created_at numeric, name text, applied_at TEXT)`, ) yield* db.run(sql` INSERT INTO __drizzle_migrations (hash, created_at, name, applied_at) VALUES ('hash', 1, '20260127222353_familiar_lady_ursula', ${new Date().toISOString()}) `) yield* DatabaseMigration.applyOnly(db, []) expect(yield* db.all(sql`SELECT id FROM migration ORDER BY id`)).toEqual([{ id: "existing" }]) }), ) }) })