feat: 品牌替换 + 启动优化 + AGENTS.md 模板定制
- 品牌替换:OpenCode/opencode → AirCoding/aircoding(16+ 文件) - Logo ASCII art:修复 left/right 行数不匹配导致的启动崩溃 - 启动诊断:添加 OPENCODE_PRINT_TIMING 计时探针 - dev 模式默认 --pure 跳过外部插件加载 - AGENTS.md 模板:追加 AirCoding 多 Agent 专项段落 - architect prompt + plugin:强化 AGENTS.md 产出验证
This commit is contained in:
232
packages/opencode/test/lsp/index.test.ts
Normal file
232
packages/opencode/test/lsp/index.test.ts
Normal file
@@ -0,0 +1,232 @@
|
||||
import { describe, expect, spyOn } from "bun:test"
|
||||
import path from "path"
|
||||
import { Deferred, Effect, Layer } from "effect"
|
||||
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 { TestInstance } from "../fixture/fixture"
|
||||
import { awaitWithTimeout, testEffect } from "../lib/effect"
|
||||
|
||||
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(lspLayer({ experimentalLspTy: true }), CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
const fakeServerPath = path.join(__dirname, "../fixture/lsp/fake-lsp-server.js")
|
||||
const disabledDownloadIt = testEffect(
|
||||
Layer.mergeAll(lspLayer({ disableLspDownload: true }), CrossSpawnSpawner.defaultLayer),
|
||||
)
|
||||
|
||||
describe("lsp.spawn", () => {
|
||||
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 {
|
||||
yield* lsp.touchFile(path.join(dir, "..", "outside.ts"))
|
||||
yield* lsp.hover({
|
||||
file: path.join(dir, "..", "hover.ts"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(0)
|
||||
} finally {
|
||||
spy.mockRestore()
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
|
||||
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 {
|
||||
yield* lsp.hover({
|
||||
file: path.join(dir, "src", "inside.ts"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(0)
|
||||
} finally {
|
||||
spy.mockRestore()
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
|
||||
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 {
|
||||
yield* lsp.hover({
|
||||
file: path.join(dir, "src", "inside.ts"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
spy.mockRestore()
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
|
||||
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 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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
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 {
|
||||
yield* lsp.hover({
|
||||
file: path.join(dir, "src", "inside.ts"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
spy.mockRestore()
|
||||
}
|
||||
}),
|
||||
),
|
||||
{
|
||||
config: {
|
||||
lsp: {
|
||||
eslint: { disabled: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
try {
|
||||
yield* lsp.hover({
|
||||
file: path.join(dir, "src", "inside.py"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(ty).toHaveBeenCalledTimes(0)
|
||||
expect(pyright).toHaveBeenCalledTimes(1)
|
||||
} finally {
|
||||
ty.mockRestore()
|
||||
pyright.mockRestore()
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
try {
|
||||
yield* lsp.hover({
|
||||
file: path.join(dir, "src", "inside.py"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(ty).toHaveBeenCalledTimes(1)
|
||||
expect(pyright).toHaveBeenCalledTimes(0)
|
||||
} finally {
|
||||
ty.mockRestore()
|
||||
pyright.mockRestore()
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
|
||||
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 {
|
||||
yield* lsp.hover({
|
||||
file: path.join(dir, "src", "inside.py"),
|
||||
line: 0,
|
||||
character: 0,
|
||||
})
|
||||
expect(pyright).toHaveBeenCalledTimes(1)
|
||||
expect(pyright.mock.calls[0]?.[2]).toMatchObject({ disableLspDownload: true })
|
||||
} finally {
|
||||
pyright.mockRestore()
|
||||
}
|
||||
}),
|
||||
),
|
||||
{ config: { lsp: true } },
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user