refactor(core): move database schema ownership (#29068)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
parent
6bcb9cb9bb
commit
7f571d36ea
@@ -1,11 +1,10 @@
|
||||
import { rm } from "fs/promises"
|
||||
import { Database } from "@/storage/db"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { disposeAllInstances } from "./fixture"
|
||||
|
||||
export async function resetDatabase() {
|
||||
await disposeAllInstances().catch(() => undefined)
|
||||
Database.close()
|
||||
const dbPath = Database.getPath()
|
||||
const dbPath = Database.path()
|
||||
await rm(dbPath, { force: true }).catch(() => undefined)
|
||||
await rm(`${dbPath}-wal`, { force: true }).catch(() => undefined)
|
||||
await rm(`${dbPath}-shm`, { force: true }).catch(() => undefined)
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { $ } from "bun"
|
||||
import * as Observability from "@opencode-ai/core/effect/observability"
|
||||
import * as fs from "fs/promises"
|
||||
import os from "os"
|
||||
import path from "path"
|
||||
import { Effect, Context, Layer, ManagedRuntime } from "effect"
|
||||
import { Effect, Context, Layer } from "effect"
|
||||
import type * as PlatformError from "effect/PlatformError"
|
||||
import type * as Scope from "effect/Scope"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
@@ -18,35 +17,31 @@ import { TestLLMServer } from "../lib/llm-server"
|
||||
|
||||
const noopBootstrap = Layer.succeed(InstanceBootstrap.Service, InstanceBootstrap.Service.of({ run: Effect.void }))
|
||||
export const testInstanceStoreLayer = InstanceStore.defaultLayer.pipe(Layer.provide(noopBootstrap))
|
||||
const testInstanceRuntime = ManagedRuntime.make(testInstanceStoreLayer.pipe(Layer.provideMerge(Observability.layer)))
|
||||
|
||||
const runTestInstanceStore = <A>(fn: (store: InstanceStore.Interface) => Effect.Effect<A>) =>
|
||||
testInstanceRuntime.runPromise(InstanceStore.Service.use(fn))
|
||||
|
||||
export async function provideTestInstance<R>(input: {
|
||||
directory: string
|
||||
init?: Effect.Effect<void>
|
||||
fn: (ctx: InstanceContext) => R
|
||||
}) {
|
||||
const ctx = await runTestInstanceStore((store) => store.load({ directory: input.directory }))
|
||||
const ctx = await InstanceRuntime.load({ directory: input.directory })
|
||||
try {
|
||||
if (input.init) await testInstanceRuntime.runPromise(input.init.pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
if (input.init) await Effect.runPromise(input.init.pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
return await input.fn(ctx)
|
||||
} finally {
|
||||
await runTestInstanceStore((store) => store.dispose(ctx))
|
||||
await InstanceRuntime.disposeInstance(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
export async function withTestInstance<R>(input: { directory: string; fn: (ctx: InstanceContext) => R }) {
|
||||
return input.fn(await runTestInstanceStore((store) => store.load({ directory: input.directory })))
|
||||
return input.fn(await InstanceRuntime.load({ directory: input.directory }))
|
||||
}
|
||||
|
||||
export async function reloadTestInstance(input: { directory: string }) {
|
||||
return runTestInstanceStore((store) => store.reload(input))
|
||||
return InstanceRuntime.reloadInstance(input)
|
||||
}
|
||||
|
||||
export async function disposeAllInstances() {
|
||||
await Promise.all([InstanceRuntime.disposeAllInstances(), runTestInstanceStore((store) => store.disposeAll())])
|
||||
await InstanceRuntime.disposeAllInstances()
|
||||
}
|
||||
|
||||
// Strip null bytes from paths (defensive fix for CI environment issues)
|
||||
@@ -119,9 +114,10 @@ export async function tmpdir<T>(options?: TmpDirOptions<T>) {
|
||||
}
|
||||
|
||||
/** Effectful scoped tmpdir. Cleaned up when the scope closes. Make sure these stay in sync */
|
||||
export function tmpdirScoped(options?: {
|
||||
export function tmpdirScoped<E = never, R = never>(options?: {
|
||||
git?: boolean
|
||||
config?: Partial<Config.Info> | (() => Partial<Config.Info>)
|
||||
init?: (directory: string) => Effect.Effect<void, E, R>
|
||||
}) {
|
||||
return Effect.gen(function* () {
|
||||
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner
|
||||
@@ -158,19 +154,16 @@ export function tmpdirScoped(options?: {
|
||||
)
|
||||
}
|
||||
|
||||
if (options?.init) yield* options.init(dir)
|
||||
|
||||
return dir
|
||||
})
|
||||
}
|
||||
|
||||
export const provideInstance =
|
||||
(directory: string) =>
|
||||
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> =>
|
||||
Effect.contextWith((services: Context.Context<R>) =>
|
||||
Effect.promise<A>(async () => {
|
||||
const ctx = await runTestInstanceStore((store) => store.load({ directory }))
|
||||
return Effect.runPromiseWith(services)(self.pipe(Effect.provideService(InstanceRef, ctx)))
|
||||
}),
|
||||
)
|
||||
<A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R | InstanceStore.Service> =>
|
||||
InstanceStore.Service.use((store) => store.provide({ directory }, self))
|
||||
|
||||
export const provideInstanceEffect =
|
||||
(directory: string) =>
|
||||
@@ -188,21 +181,8 @@ export function provideTmpdirInstance<A, E, R>(
|
||||
) {
|
||||
return Effect.gen(function* () {
|
||||
const path = yield* tmpdirScoped(options)
|
||||
let provided = false
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
provided
|
||||
? Effect.promise(() =>
|
||||
runTestInstanceStore((store) =>
|
||||
store.load({ directory: path }).pipe(Effect.flatMap((ctx) => store.dispose(ctx))),
|
||||
),
|
||||
).pipe(Effect.ignore)
|
||||
: Effect.void,
|
||||
)
|
||||
|
||||
provided = true
|
||||
return yield* self(path).pipe(provideInstance(path))
|
||||
})
|
||||
}).pipe(Effect.provide(testInstanceStoreLayer))
|
||||
}
|
||||
|
||||
export class TestInstance extends Context.Service<TestInstance, { readonly directory: string }>()("@test/Instance") {}
|
||||
@@ -214,7 +194,11 @@ export const requireInstance = Effect.gen(function* () {
|
||||
})
|
||||
|
||||
export const withTmpdirInstance =
|
||||
(options?: { git?: boolean; config?: Partial<Config.Info> | (() => Partial<Config.Info>) }) =>
|
||||
<E2 = never, R2 = never>(options?: {
|
||||
git?: boolean
|
||||
config?: Partial<Config.Info> | (() => Partial<Config.Info>)
|
||||
init?: (directory: string) => Effect.Effect<void, E2, R2>
|
||||
}) =>
|
||||
<A, E, R>(self: Effect.Effect<A, E, R>) =>
|
||||
Effect.gen(function* () {
|
||||
const directory = yield* tmpdirScoped(options)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { WorkspaceID } from "@/control-plane/schema"
|
||||
import type { WorkspaceV2 } from "@opencode-ai/core/workspace"
|
||||
import { Flag } from "@opencode-ai/core/flag/flag"
|
||||
import { Effect, Scope } from "effect"
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Effect, Scope } from "effect"
|
||||
* on entry and restores it via finalizer when the surrounding scope closes —
|
||||
* preserves the original try/finally semantics regardless of test outcome.
|
||||
*/
|
||||
export function withFixedWorkspaceID(id: WorkspaceID): Effect.Effect<void, never, Scope.Scope> {
|
||||
export function withFixedWorkspaceID(id: WorkspaceV2.ID): Effect.Effect<void, never, Scope.Scope> {
|
||||
return Effect.gen(function* () {
|
||||
const previous = Flag.OPENCODE_WORKSPACE_ID
|
||||
Flag.OPENCODE_WORKSPACE_ID = id
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { FetchHttpClient } from "effect/unstable/http"
|
||||
import { Layer } from "effect"
|
||||
import { Database } from "@opencode-ai/core/database/database"
|
||||
import { AppFileSystem } from "@opencode-ai/core/filesystem"
|
||||
import { Auth } from "../../src/auth"
|
||||
import { Workspace } from "../../src/control-plane/workspace"
|
||||
@@ -10,16 +11,17 @@ import { Project } from "../../src/project/project"
|
||||
import { Vcs } from "../../src/project/vcs"
|
||||
import { Session } from "../../src/session/session"
|
||||
import { SessionPrompt } from "../../src/session/prompt"
|
||||
import { SyncEvent } from "../../src/sync"
|
||||
import { EventV2Bridge } from "../../src/event-v2-bridge"
|
||||
|
||||
export const workspaceLayerWithRuntimeFlags = (overrides: Partial<RuntimeFlags.Info>) =>
|
||||
Workspace.layer.pipe(
|
||||
Layer.provide(Auth.defaultLayer),
|
||||
Layer.provide(Session.defaultLayer),
|
||||
Layer.provide(SyncEvent.defaultLayer),
|
||||
Layer.provide(SessionPrompt.defaultLayer),
|
||||
Layer.provide(Project.defaultLayer),
|
||||
Layer.provide(Vcs.defaultLayer),
|
||||
Layer.provide(Database.defaultLayer),
|
||||
Layer.provide(EventV2Bridge.defaultLayer),
|
||||
Layer.provide(FetchHttpClient.layer),
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer(overrides)),
|
||||
|
||||
Reference in New Issue
Block a user