From 71423b9a5825eed2a018b5cc118dcf26f19ed6f1 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Wed, 20 May 2026 20:49:11 -0400 Subject: [PATCH] refactor(v2): keep test database setup explicit --- packages/opencode/src/storage/db.ts | 26 ++++++------- packages/opencode/src/v2/storage/database.ts | 38 +++++++++---------- .../opencode/test/v2/session-storage.test.ts | 23 ++++++++++- 3 files changed, 52 insertions(+), 35 deletions(-) diff --git a/packages/opencode/src/storage/db.ts b/packages/opencode/src/storage/db.ts index 6df731b530..06f1f84a9a 100644 --- a/packages/opencode/src/storage/db.ts +++ b/packages/opencode/src/storage/db.ts @@ -1,6 +1,5 @@ import { type SQLiteBunDatabase } from "drizzle-orm/bun-sqlite" import { migrate } from "drizzle-orm/bun-sqlite/migrator" -import type { MigrationsJournal } from "drizzle-orm/migrator" import { type SQLiteTransaction } from "drizzle-orm/sqlite-core" export * from "drizzle-orm" import { RuntimeFlags } from "@/effect/runtime-flags" @@ -48,10 +47,13 @@ export type Transaction = SQLiteTransaction<"sync", void> type Client = ReturnType -type Journal = MigrationsJournal +type Journal = { sql: string; timestamp: number; name: string }[] + +// Drizzle's migrate overloads trigger expensive variance checks here; narrow to the journal overload we actually use. +const migrateFromJournal = migrate as unknown as (db: SQLiteBunDatabase, entries: Journal) => void function applyMigrations(db: SQLiteBunDatabase, entries: Journal) { - migrate(db, entries) + migrateFromJournal(db, entries) } function time(tag: string) { @@ -72,17 +74,17 @@ function migrations(dir: string): Journal { .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) - const sql: Journal = dirs + const sql = dirs .map((name) => { const file = path.join(dir, name, "migration.sql") - if (!existsSync(file)) return undefined + if (!existsSync(file)) return return { sql: readFileSync(file, "utf-8"), timestamp: time(name), name, } }) - .filter((entry) => entry !== undefined) + .filter(Boolean) as Journal return sql.sort((a, b) => a.timestamp - b.timestamp) } @@ -92,7 +94,7 @@ let loaded = false export const Client = Object.assign( (flags: DatabaseFlags = readRuntimeFlags()): Client => { - if (loaded && client) return client + if (loaded) return client as Client const dbPath = getPath(flags) log.info("opening database", { path: dbPath }) @@ -157,19 +159,19 @@ export function use(callback: (trx: TxOrDb) => T): T { if (err instanceof LocalContext.NotFound) { const effects: (() => void | Promise)[] = [] const result = ctx.provide({ effects, tx: Client() }, () => callback(Client())) - for (const effect of effects) void effect() + for (const effect of effects) effect() return result } throw err } } -export function effect(fn: () => void | Promise) { +export function effect(fn: () => any | Promise) { const bound = EffectBridge.bind(fn) try { ctx.use().effects.push(bound) } catch { - void bound() + bound() } } @@ -188,9 +190,7 @@ export function transaction( const effects: (() => void | Promise)[] = [] const txCallback = EffectBridge.bind((tx: TxOrDb) => ctx.provide({ tx, effects }, () => callback(tx))) const result = Client().transaction(txCallback, { behavior: options?.behavior }) - for (const effect of effects) void effect() - // Drizzle's transaction type does not preserve our NotPromise constraint through the callback wrapper. - // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion + for (const effect of effects) effect() return result as NotPromise } throw err diff --git a/packages/opencode/src/v2/storage/database.ts b/packages/opencode/src/v2/storage/database.ts index 47c5a375a6..7ac5db016a 100644 --- a/packages/opencode/src/v2/storage/database.ts +++ b/packages/opencode/src/v2/storage/database.ts @@ -2,35 +2,31 @@ import { Database as LegacyDatabase } from "@/storage/db" import { SqliteClient } from "@effect/sql-sqlite-bun" import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" import { Context, Effect, Layer } from "effect" -import path from "path" const makeDatabase = EffectDrizzleSqlite.makeWithDefaults() type DatabaseShape = Effect.Success export class Service extends Context.Service()("@opencode/v2/storage/Database") {} +export const layerForPath = (filename: string) => + Layer.effect( + Service, + Effect.gen(function* () { + const db = yield* makeDatabase + yield* db.run("PRAGMA journal_mode = WAL") + yield* db.run("PRAGMA synchronous = NORMAL") + yield* db.run("PRAGMA busy_timeout = 5000") + yield* db.run("PRAGMA cache_size = -64000") + yield* db.run("PRAGMA foreign_keys = ON") + yield* db.run("PRAGMA wal_checkpoint(PASSIVE)") + return db + }), + ).pipe(Layer.provide(SqliteClient.layer({ filename }))) + export const layer = Layer.unwrap( Effect.sync(() => { - const filename = LegacyDatabase.getPath() - return Layer.effect( - Service, - Effect.gen(function* () { - LegacyDatabase.Client() - const db = yield* makeDatabase - yield* db.run("PRAGMA journal_mode = WAL") - yield* db.run("PRAGMA synchronous = NORMAL") - yield* db.run("PRAGMA busy_timeout = 5000") - yield* db.run("PRAGMA cache_size = -64000") - yield* db.run("PRAGMA foreign_keys = ON") - yield* db.run("PRAGMA wal_checkpoint(PASSIVE)") - if (filename === ":memory:") { - yield* EffectDrizzleSqlite.migrate(db, { - migrationsFolder: path.join(import.meta.dirname, "../../../migration"), - }) - } - return db - }), - ).pipe(Layer.provide(SqliteClient.layer({ filename, disableWAL: filename === ":memory:" }))) + LegacyDatabase.Client() + return layerForPath(LegacyDatabase.getPath()) }), ) diff --git a/packages/opencode/test/v2/session-storage.test.ts b/packages/opencode/test/v2/session-storage.test.ts index 7083950a1b..55932e5f47 100644 --- a/packages/opencode/test/v2/session-storage.test.ts +++ b/packages/opencode/test/v2/session-storage.test.ts @@ -9,8 +9,12 @@ import { SessionStorageMemory } from "@/v2/storage/session-memory" import { SessionStorageSql } from "@/v2/storage/session-sql" import { EventV2 } from "@opencode-ai/core/event" import { SessionMessage } from "@opencode-ai/core/session-message" +import { EffectDrizzleSqlite } from "@opencode-ai/effect-drizzle-sqlite" import { eq, or } from "@/storage/db" import { DateTime, Effect, Layer, Schema } from "effect" +import fs from "fs/promises" +import os from "os" +import path from "path" import { testEffect } from "../lib/effect" const projectID = ProjectID.make("project-session-storage") @@ -157,7 +161,24 @@ function sessionStorageContract(name: string, layer: Layer.Layer fs.mkdtemp(path.join(os.tmpdir(), "opencode-storage-test-"))), + (dir) => Effect.promise(() => fs.rm(dir, { recursive: true, force: true })), + ) + return Layer.effect( + StorageDatabase.Service, + Effect.gen(function* () { + const db = yield* StorageDatabase.Service + yield* EffectDrizzleSqlite.migrate(db, { migrationsFolder: path.join(import.meta.dirname, "../../migration") }) + return db + }), + ).pipe(Layer.provide(StorageDatabase.layerForPath(path.join(dir, "storage.db")))) + }), +) + +const sqlLayer = SessionStorageSql.layer.pipe(Layer.provideMerge(testDatabaseLayer)) const sqlSeeds: Seeds = { reset: resetSqlSeeds(),