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:
@@ -1,36 +1,44 @@
|
||||
import { describe, expect, spyOn } from "bun:test"
|
||||
import path from "path"
|
||||
import { Deferred, Effect, Layer } from "effect"
|
||||
import { Bus } from "@/bus"
|
||||
import { EventV2Bridge } from "@/event-v2-bridge"
|
||||
import { Config } from "@/config/config"
|
||||
import { RuntimeFlags } from "@/effect/runtime-flags"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import * as LSPServer from "@/lsp/server"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
import { awaitWithTimeout, testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
const lspLayer = (flags: Parameters<typeof RuntimeFlags.layer>[0] = {}) =>
|
||||
LSP.layer.pipe(
|
||||
Layer.provide(Config.defaultLayer),
|
||||
Layer.provide(RuntimeFlags.layer(flags)),
|
||||
Layer.provideMerge(EventV2Bridge.defaultLayer),
|
||||
)
|
||||
|
||||
const it = testEffect(Layer.mergeAll(lspLayer(), CrossSpawnSpawner.defaultLayer))
|
||||
const experimentalTyIt = testEffect(
|
||||
Layer.mergeAll(
|
||||
LSP.layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(RuntimeFlags.layer({ experimentalLspTy: true }))),
|
||||
lspLayer({ experimentalLspTy: true }),
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
),
|
||||
)
|
||||
const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
|
||||
const disabledDownloadIt = testEffect(
|
||||
Layer.mergeAll(
|
||||
LSP.layer.pipe(Layer.provide(Config.defaultLayer), Layer.provide(RuntimeFlags.layer({ disableLspDownload: true }))),
|
||||
lspLayer({ disableLspDownload: true }),
|
||||
CrossSpawnSpawner.defaultLayer,
|
||||
),
|
||||
)
|
||||
|
||||
describe("lsp.spawn", () => {
|
||||
it.live("does not spawn builtin LSP for files outside instance", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
it.instance(
|
||||
"does not spawn builtin LSP for files outside instance",
|
||||
() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const dir = (yield* TestInstance).directory
|
||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||
|
||||
try {
|
||||
@@ -46,14 +54,13 @@ describe("lsp.spawn", () => {
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
|
||||
it.live("does not spawn builtin LSP for files inside instance when LSP is unset", () =>
|
||||
provideTmpdirInstance((dir) =>
|
||||
it.instance("does not spawn builtin LSP for files inside instance when LSP is unset", () =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const dir = (yield* TestInstance).directory
|
||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||
|
||||
try {
|
||||
@@ -68,14 +75,14 @@ describe("lsp.spawn", () => {
|
||||
}
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("would spawn builtin LSP for files inside instance when lsp is true", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
it.instance(
|
||||
"would spawn builtin LSP for files inside instance when lsp is true",
|
||||
() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const dir = (yield* TestInstance).directory
|
||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||
|
||||
try {
|
||||
@@ -90,44 +97,46 @@ describe("lsp.spawn", () => {
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
|
||||
it.live("publishes lsp.updated after custom LSP initialization", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
Effect.gen(function* () {
|
||||
it.instance(
|
||||
"publishes lsp.updated after custom LSP initialization",
|
||||
() =>
|
||||
Effect.gen(function* () {
|
||||
const dir = (yield* TestInstance).directory
|
||||
const lsp = yield* LSP.Service
|
||||
const updated = yield* Deferred.make<void>()
|
||||
const unsubscribe = Bus.subscribe(LSP.Event.Updated, () =>
|
||||
Effect.runSync(Deferred.succeed(updated, undefined)),
|
||||
)
|
||||
yield* Effect.addFinalizer(() => Effect.sync(unsubscribe))
|
||||
const events = yield* EventV2Bridge.Service
|
||||
const unsubscribe = yield* events.listen((event) => {
|
||||
if (event.type === LSP.Event.Updated.type) Deferred.doneUnsafe(updated, Effect.void)
|
||||
return Effect.void
|
||||
})
|
||||
yield* Effect.addFinalizer(() => unsubscribe)
|
||||
|
||||
const file = path.join(dir, "sample.repro")
|
||||
yield* Effect.promise(() => Bun.write(file, "sample\n"))
|
||||
yield* lsp.touchFile(file)
|
||||
yield* awaitWithTimeout(Deferred.await(updated), "lsp.updated event was not published")
|
||||
}),
|
||||
{
|
||||
config: {
|
||||
lsp: {
|
||||
fake: {
|
||||
command: [process.execPath, fakeServerPath],
|
||||
extensions: [".repro"],
|
||||
},
|
||||
{
|
||||
config: {
|
||||
lsp: {
|
||||
fake: {
|
||||
command: [process.execPath, fakeServerPath],
|
||||
extensions: [".repro"],
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
it.live("would spawn builtin LSP for files inside instance when config object is provided", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
it.instance(
|
||||
"would spawn builtin LSP for files inside instance when config object is provided",
|
||||
() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const dir = (yield* TestInstance).directory
|
||||
const spy = spyOn(LSPServer.Typescript, "spawn").mockResolvedValue(undefined)
|
||||
|
||||
try {
|
||||
@@ -142,21 +151,21 @@ describe("lsp.spawn", () => {
|
||||
}
|
||||
}),
|
||||
),
|
||||
{
|
||||
config: {
|
||||
lsp: {
|
||||
eslint: { disabled: true },
|
||||
},
|
||||
{
|
||||
config: {
|
||||
lsp: {
|
||||
eslint: { disabled: true },
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
)
|
||||
|
||||
it.live("uses pyright instead of ty by default", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
it.instance(
|
||||
"uses pyright instead of ty by default",
|
||||
() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const dir = (yield* TestInstance).directory
|
||||
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
|
||||
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
|
||||
|
||||
@@ -174,15 +183,15 @@ describe("lsp.spawn", () => {
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
|
||||
experimentalTyIt.live("uses ty instead of pyright when experimentalLspTy is enabled", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
experimentalTyIt.instance(
|
||||
"uses ty instead of pyright when experimentalLspTy is enabled",
|
||||
() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const dir = (yield* TestInstance).directory
|
||||
const ty = spyOn(LSPServer.Ty, "spawn").mockResolvedValue(undefined)
|
||||
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
|
||||
|
||||
@@ -200,15 +209,15 @@ describe("lsp.spawn", () => {
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
|
||||
disabledDownloadIt.live("passes disableLspDownload to builtin LSP spawn", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
disabledDownloadIt.instance(
|
||||
"passes disableLspDownload to builtin LSP spawn",
|
||||
() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const dir = (yield* TestInstance).directory
|
||||
const pyright = spyOn(LSPServer.Pyright, "spawn").mockResolvedValue(undefined)
|
||||
|
||||
try {
|
||||
@@ -224,7 +233,6 @@ describe("lsp.spawn", () => {
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Effect, Layer } from "effect"
|
||||
import { LSP } from "@/lsp/lsp"
|
||||
import * as LSPServer from "@/lsp/server"
|
||||
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { TestInstance } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
const it = testEffect(Layer.mergeAll(LSP.defaultLayer, CrossSpawnSpawner.defaultLayer))
|
||||
@@ -20,137 +20,113 @@ describe("LSP service lifecycle", () => {
|
||||
spawnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it.live("init() completes without error", () => provideTmpdirInstance(() => LSP.Service.use((lsp) => lsp.init())))
|
||||
it.instance("init() completes without error", () => LSP.Service.use((lsp) => lsp.init()))
|
||||
|
||||
it.live("status() returns empty array initially", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.status()
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect(result.length).toBe(0)
|
||||
}),
|
||||
),
|
||||
it.instance("status() returns empty array initially", () =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.status()
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect(result.length).toBe(0)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("diagnostics() returns empty object initially", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.diagnostics()
|
||||
expect(typeof result).toBe("object")
|
||||
expect(Object.keys(result).length).toBe(0)
|
||||
}),
|
||||
),
|
||||
it.instance("diagnostics() returns empty object initially", () =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.diagnostics()
|
||||
expect(typeof result).toBe("object")
|
||||
expect(Object.keys(result).length).toBe(0)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("hasClients() returns false for .ts files in instance when LSP is unset", () =>
|
||||
provideTmpdirInstance((dir) =>
|
||||
it.instance("hasClients() returns false for .ts files in instance when LSP is unset", () =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
|
||||
const result = yield* lsp.hasClients(path.join((yield* TestInstance).directory, "test.ts"))
|
||||
expect(result).toBe(false)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("hasClients() returns true for .ts files in instance when lsp is true", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
|
||||
expect(result).toBe(true)
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
),
|
||||
)
|
||||
|
||||
it.live("hasClients() keeps built-in LSPs when config object is provided", () =>
|
||||
provideTmpdirInstance(
|
||||
(dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.hasClients(path.join(dir, "test.ts"))
|
||||
expect(result).toBe(true)
|
||||
}),
|
||||
),
|
||||
{
|
||||
config: {
|
||||
lsp: {
|
||||
eslint: { disabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
it.live("hasClients() returns false for files outside instance", () =>
|
||||
provideTmpdirInstance((dir) =>
|
||||
it.instance(
|
||||
"hasClients() returns true for .ts files in instance when lsp is true",
|
||||
() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.hasClients(path.join(dir, "..", "outside.ts"))
|
||||
expect(typeof result).toBe("boolean")
|
||||
const result = yield* lsp.hasClients(path.join((yield* TestInstance).directory, "test.ts"))
|
||||
expect(result).toBe(true)
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
|
||||
it.instance(
|
||||
"hasClients() keeps built-in LSPs when config object is provided",
|
||||
() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.hasClients(path.join((yield* TestInstance).directory, "test.ts"))
|
||||
expect(result).toBe(true)
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: { eslint: { disabled: true } } } },
|
||||
)
|
||||
|
||||
it.instance("hasClients() returns false for files outside instance", () =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.hasClients(path.join((yield* TestInstance).directory, "..", "outside.ts"))
|
||||
expect(typeof result).toBe("boolean")
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("workspaceSymbol() returns empty array with no clients", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.workspaceSymbol("test")
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect(result.length).toBe(0)
|
||||
}),
|
||||
),
|
||||
it.instance("workspaceSymbol() returns empty array with no clients", () =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.workspaceSymbol("test")
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
expect(result.length).toBe(0)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("definition() returns empty array for unknown file", () =>
|
||||
provideTmpdirInstance((dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.definition({
|
||||
file: path.join(dir, "nonexistent.ts"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
}),
|
||||
),
|
||||
it.instance("definition() returns empty array for unknown file", () =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.definition({
|
||||
file: path.join((yield* TestInstance).directory, "nonexistent.ts"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("references() returns empty array for unknown file", () =>
|
||||
provideTmpdirInstance((dir) =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.references({
|
||||
file: path.join(dir, "nonexistent.ts"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
}),
|
||||
),
|
||||
it.instance("references() returns empty array for unknown file", () =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
const result = yield* lsp.references({
|
||||
file: path.join((yield* TestInstance).directory, "nonexistent.ts"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(Array.isArray(result)).toBe(true)
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
it.live("multiple init() calls are idempotent", () =>
|
||||
provideTmpdirInstance(() =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
yield* lsp.init()
|
||||
yield* lsp.init()
|
||||
yield* lsp.init()
|
||||
}),
|
||||
),
|
||||
it.instance("multiple init() calls are idempotent", () =>
|
||||
LSP.Service.use((lsp) =>
|
||||
Effect.gen(function* () {
|
||||
yield* lsp.init()
|
||||
yield* lsp.init()
|
||||
yield* lsp.init()
|
||||
}),
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user