mirror of
https://github.com/anomalyco/opencode.git
synced 2026-04-16 02:44:49 +00:00
Compare commits
1 Commits
dev
...
kit/tui-fa
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8d6379e0e |
@@ -4,6 +4,7 @@ import { tui } from "./app"
|
||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { existsSync } from "fs"
|
||||
|
||||
export const AttachCommand = cmd({
|
||||
@@ -68,7 +69,7 @@ export const AttachCommand = cmd({
|
||||
})()
|
||||
const config = await Instance.provide({
|
||||
directory: directory && existsSync(directory) ? directory : process.cwd(),
|
||||
fn: () => TuiConfig.get(),
|
||||
fn: () => AppRuntime.runPromise(TuiConfig.Service.use((svc) => svc.get())),
|
||||
})
|
||||
await tui({
|
||||
url: args.url,
|
||||
|
||||
@@ -15,6 +15,7 @@ import { fileURLToPath } from "url"
|
||||
|
||||
import { Config } from "@/config/config"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { Log } from "@/util/log"
|
||||
import { errorData, errorMessage } from "@/util/error"
|
||||
import { isRecord } from "@/util/record"
|
||||
@@ -794,7 +795,10 @@ async function addPluginBySpec(state: RuntimeState | undefined, raw: string) {
|
||||
|
||||
const ready = await Instance.provide({
|
||||
directory: state.directory,
|
||||
fn: () => resolveExternalPlugins([cfg], () => TuiConfig.waitForDependencies()),
|
||||
fn: () =>
|
||||
resolveExternalPlugins([cfg], () =>
|
||||
AppRuntime.runPromise(TuiConfig.Service.use((svc) => svc.waitForDependencies())),
|
||||
),
|
||||
}).catch((error) => {
|
||||
fail("failed to add tui plugin", { path: next, error })
|
||||
return [] as PluginLoad[]
|
||||
@@ -991,7 +995,7 @@ export namespace TuiPluginRuntime {
|
||||
await Instance.provide({
|
||||
directory: cwd,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await AppRuntime.runPromise(TuiConfig.Service.use((svc) => svc.get()))
|
||||
const records = Flag.OPENCODE_PURE ? [] : (config.plugin_origins ?? [])
|
||||
if (Flag.OPENCODE_PURE && config.plugin_origins?.length) {
|
||||
log.info("skipping external tui plugins in pure mode", { count: config.plugin_origins.length })
|
||||
@@ -1011,7 +1015,9 @@ export namespace TuiPluginRuntime {
|
||||
})
|
||||
}
|
||||
|
||||
const ready = await resolveExternalPlugins(records, () => TuiConfig.waitForDependencies())
|
||||
const ready = await resolveExternalPlugins(records, () =>
|
||||
AppRuntime.runPromise(TuiConfig.Service.use((svc) => svc.waitForDependencies())),
|
||||
)
|
||||
await addExternalPluginEntries(next, ready)
|
||||
|
||||
applyInitialPluginEnabledState(next, config)
|
||||
|
||||
@@ -15,6 +15,7 @@ import type { EventSource } from "./context/sdk"
|
||||
import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
import { Instance } from "@/project/instance"
|
||||
import { AppRuntime } from "@/effect/app-runtime"
|
||||
import { writeHeapSnapshot } from "v8"
|
||||
|
||||
declare global {
|
||||
@@ -179,7 +180,7 @@ export const TuiThreadCommand = cmd({
|
||||
const prompt = await input(args.prompt)
|
||||
const config = await Instance.provide({
|
||||
directory: cwd,
|
||||
fn: () => TuiConfig.get(),
|
||||
fn: () => AppRuntime.runPromise(TuiConfig.Service.use((svc) => svc.get())),
|
||||
})
|
||||
|
||||
const network = await resolveNetworkOptions(args)
|
||||
|
||||
@@ -12,7 +12,6 @@ import { isRecord } from "@/util/record"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { InstanceState } from "@/effect/instance-state"
|
||||
import { makeRuntime } from "@/effect/run-service"
|
||||
import { AppFileSystem } from "@/filesystem"
|
||||
|
||||
export namespace TuiConfig {
|
||||
@@ -170,16 +169,6 @@ export namespace TuiConfig {
|
||||
|
||||
export const defaultLayer = layer.pipe(Layer.provide(Config.defaultLayer))
|
||||
|
||||
const { runPromise } = makeRuntime(Service, defaultLayer)
|
||||
|
||||
export async function get() {
|
||||
return runPromise((svc) => svc.get())
|
||||
}
|
||||
|
||||
export async function waitForDependencies() {
|
||||
await runPromise((svc) => svc.waitForDependencies())
|
||||
}
|
||||
|
||||
async function loadFile(filepath: string): Promise<Info> {
|
||||
const text = await ConfigPaths.readFile(filepath)
|
||||
if (!text) return {}
|
||||
|
||||
@@ -47,6 +47,7 @@ import { Pty } from "@/pty"
|
||||
import { Installation } from "@/installation"
|
||||
import { ShareNext } from "@/share/share-next"
|
||||
import { SessionShare } from "@/share/session"
|
||||
import { TuiConfig } from "@/config/tui"
|
||||
|
||||
export const AppLayer = Layer.mergeAll(
|
||||
Observability.layer,
|
||||
@@ -95,6 +96,7 @@ export const AppLayer = Layer.mergeAll(
|
||||
Installation.defaultLayer,
|
||||
ShareNext.defaultLayer,
|
||||
SessionShare.defaultLayer,
|
||||
TuiConfig.defaultLayer,
|
||||
)
|
||||
|
||||
const rt = ManagedRuntime.make(AppLayer, { memoMap })
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { expect, spyOn, test } from "bun:test"
|
||||
import fs from "fs/promises"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { pathToFileURL } from "url"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { TuiConfig } from "../../../src/config/tui"
|
||||
import { mockTuiService } from "../../fixture/tui-runtime"
|
||||
|
||||
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
|
||||
|
||||
@@ -31,11 +32,10 @@ test("adds tui plugin at runtime from spec", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [],
|
||||
plugin_origins: undefined,
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
@@ -54,8 +54,7 @@ test("adds tui plugin at runtime from spec", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
@@ -72,22 +71,27 @@ test("retries runtime add for file plugins after dependency wait", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
plugin: [],
|
||||
plugin_origins: undefined,
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockImplementation(async () => {
|
||||
await Bun.write(
|
||||
path.join(tmp.extra.mod, "index.ts"),
|
||||
`export default {
|
||||
const restore = mockTuiService(
|
||||
{
|
||||
plugin: [],
|
||||
plugin_origins: undefined,
|
||||
},
|
||||
{
|
||||
wait: () =>
|
||||
Effect.promise(async () => {
|
||||
await Bun.write(
|
||||
path.join(tmp.extra.mod, "index.ts"),
|
||||
`export default {
|
||||
id: "demo.add.retry",
|
||||
tui: async () => {
|
||||
await Bun.write(${JSON.stringify(tmp.extra.marker)}, "called")
|
||||
},
|
||||
}
|
||||
`,
|
||||
)
|
||||
})
|
||||
)
|
||||
}),
|
||||
},
|
||||
)
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
@@ -95,13 +99,11 @@ test("retries runtime add for file plugins after dependency wait", async () => {
|
||||
|
||||
await expect(TuiPluginRuntime.addPlugin(tmp.extra.spec)).resolves.toBe(true)
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("called")
|
||||
expect(wait).toHaveBeenCalledTimes(1)
|
||||
expect(TuiPluginRuntime.list().find((item) => item.id === "demo.add.retry")?.active).toBe(true)
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { pathToFileURL } from "url"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { TuiConfig } from "../../../src/config/tui"
|
||||
import { mockTuiService } from "../../fixture/tui-runtime"
|
||||
|
||||
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
|
||||
|
||||
@@ -50,12 +51,11 @@ test("installs plugin without loading it", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const cfg: Awaited<ReturnType<typeof TuiConfig.get>> = {
|
||||
const cfg: TuiConfig.Info = {
|
||||
plugin: [],
|
||||
plugin_origins: undefined,
|
||||
}
|
||||
const get = spyOn(TuiConfig, "get").mockImplementation(async () => cfg)
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const restore = mockTuiService(cfg)
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const api = createTuiPluginApi({
|
||||
state: {
|
||||
@@ -82,8 +82,7 @@ test("installs plugin without loading it", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
|
||||
@@ -5,7 +5,6 @@ import { pathToFileURL } from "url"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { mockTuiRuntime } from "../../fixture/tui-runtime"
|
||||
import { TuiConfig } from "../../../src/config/tui"
|
||||
|
||||
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { TuiConfig } from "../../../src/config/tui"
|
||||
import { mockTuiService } from "../../fixture/tui-runtime"
|
||||
import { Npm } from "../../../src/npm"
|
||||
|
||||
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
|
||||
@@ -44,7 +44,7 @@ test("loads npm tui plugin from package ./tui export", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -54,7 +54,6 @@ test("loads npm tui plugin from package ./tui export", async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
|
||||
|
||||
@@ -69,8 +68,7 @@ test("loads npm tui plugin from package ./tui export", async () => {
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
@@ -106,7 +104,7 @@ test("does not use npm package exports dot for tui entry", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -116,7 +114,6 @@ test("does not use npm package exports dot for tui entry", async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
|
||||
|
||||
@@ -128,8 +125,7 @@ test("does not use npm package exports dot for tui entry", async () => {
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
@@ -169,7 +165,7 @@ test("rejects npm tui export that resolves outside plugin directory", async () =
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -179,7 +175,6 @@ test("rejects npm tui export that resolves outside plugin directory", async () =
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
|
||||
|
||||
@@ -193,8 +188,7 @@ test("rejects npm tui export that resolves outside plugin directory", async () =
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
@@ -232,7 +226,7 @@ test("rejects npm tui plugin that exports server and tui together", async () =>
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -242,7 +236,6 @@ test("rejects npm tui plugin that exports server and tui together", async () =>
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
|
||||
|
||||
@@ -254,8 +247,7 @@ test("rejects npm tui plugin that exports server and tui together", async () =>
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
@@ -291,7 +283,7 @@ test("does not use npm package main for tui entry", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -301,7 +293,6 @@ test("does not use npm package main for tui entry", async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
|
||||
const warn = spyOn(console, "warn").mockImplementation(() => {})
|
||||
@@ -317,8 +308,7 @@ test("does not use npm package main for tui entry", async () => {
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
warn.mockRestore()
|
||||
error.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
@@ -357,7 +347,7 @@ test("does not use directory package main for tui entry", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -367,7 +357,6 @@ test("does not use directory package main for tui entry", async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
@@ -377,8 +366,7 @@ test("does not use directory package main for tui entry", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
@@ -405,7 +393,7 @@ test("uses directory index fallback for tui when package.json is missing", async
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -415,7 +403,6 @@ test("uses directory index fallback for tui when package.json is missing", async
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
@@ -425,8 +412,7 @@ test("uses directory index fallback for tui when package.json is missing", async
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
@@ -463,7 +449,7 @@ test("uses npm package name when tui plugin id is omitted", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -473,7 +459,6 @@ test("uses npm package name when tui plugin id is omitted", async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: tmp.extra.mod })
|
||||
|
||||
@@ -485,8 +470,7 @@ test("uses npm package name when tui plugin id is omitted", async () => {
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { TuiConfig } from "../../../src/config/tui"
|
||||
import { mockTuiService } from "../../fixture/tui-runtime"
|
||||
|
||||
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
|
||||
|
||||
@@ -37,7 +37,7 @@ test("skips external tui plugins in pure mode", async () => {
|
||||
process.env.OPENCODE_PURE = "1"
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = tmp.extra.meta
|
||||
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -47,7 +47,6 @@ test("skips external tui plugins in pure mode", async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
@@ -56,8 +55,7 @@ test("skips external tui plugins in pure mode", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
if (pure === undefined) {
|
||||
delete process.env.OPENCODE_PURE
|
||||
} else {
|
||||
|
||||
@@ -4,8 +4,8 @@ import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { mockTuiService } from "../../fixture/tui-runtime"
|
||||
import { Global } from "../../../src/global"
|
||||
import { TuiConfig } from "../../../src/config/tui"
|
||||
import { Filesystem } from "../../../src/util/filesystem"
|
||||
|
||||
const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme")
|
||||
@@ -322,8 +322,59 @@ export default {
|
||||
}
|
||||
},
|
||||
})
|
||||
const localConfigPath = path.join(tmp.path, "tui.json")
|
||||
const globalPlugin: [string, Record<string, unknown>] = [
|
||||
tmp.extra.globalSpec,
|
||||
{
|
||||
marker: tmp.extra.globalMarker,
|
||||
theme_path: `./${tmp.extra.globalThemeFile}`,
|
||||
theme_name: tmp.extra.globalThemeName,
|
||||
},
|
||||
]
|
||||
const localPlugins: [string, Record<string, unknown>][] = [
|
||||
[
|
||||
tmp.extra.localSpec,
|
||||
{
|
||||
fn_marker: tmp.extra.fnMarker,
|
||||
marker: tmp.extra.localMarker,
|
||||
source: path.join(tmp.path, tmp.extra.localThemeFile),
|
||||
dest: tmp.extra.localDest,
|
||||
theme_path: `./${tmp.extra.localThemeFile}`,
|
||||
theme_name: tmp.extra.localThemeName,
|
||||
kv_key: "plugin_state_key",
|
||||
session_id: "ses_test",
|
||||
keybinds: {
|
||||
modal: "ctrl+alt+m",
|
||||
close: "q",
|
||||
},
|
||||
},
|
||||
],
|
||||
[
|
||||
tmp.extra.invalidSpec,
|
||||
{
|
||||
marker: tmp.extra.invalidMarker,
|
||||
theme_path: `./${tmp.extra.invalidThemeFile}`,
|
||||
theme_name: tmp.extra.invalidThemeName,
|
||||
},
|
||||
],
|
||||
[
|
||||
tmp.extra.preloadedSpec,
|
||||
{
|
||||
marker: tmp.extra.preloadedMarker,
|
||||
dest: tmp.extra.preloadedDest,
|
||||
theme_path: `./${tmp.extra.preloadedThemeFile}`,
|
||||
theme_name: tmp.extra.preloadedThemeName,
|
||||
},
|
||||
],
|
||||
]
|
||||
const restore = mockTuiService({
|
||||
plugin: [globalPlugin, ...localPlugins],
|
||||
plugin_origins: [
|
||||
{ spec: globalPlugin, scope: "global", source: globalConfigPath },
|
||||
...localPlugins.map((spec) => ({ spec, scope: "local" as const, source: localConfigPath })),
|
||||
],
|
||||
})
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
|
||||
try {
|
||||
expect(addTheme(tmp.extra.preloadedThemeName, { theme: { primary: "#303030" } })).toBe(true)
|
||||
@@ -404,7 +455,7 @@ export default {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
if (backup === undefined) {
|
||||
await fs.rm(globalConfigPath, { force: true })
|
||||
} else {
|
||||
@@ -459,7 +510,7 @@ test("continues loading when a plugin is missing config metadata", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [
|
||||
[tmp.extra.badSpec, { marker: path.join(tmp.path, "bad.txt") }],
|
||||
[tmp.extra.goodSpec, { marker: tmp.extra.goodMarker }],
|
||||
@@ -478,7 +529,6 @@ test("continues loading when a plugin is missing config metadata", async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
@@ -492,8 +542,7 @@ test("continues loading when a plugin is missing config metadata", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
@@ -696,8 +745,12 @@ test("updates installed theme when plugin metadata changes", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const plugin: [string, Record<string, unknown>] = [tmp.extra.spec, { theme_path: "./theme-update.json" }]
|
||||
const restore = mockTuiService({
|
||||
plugin: [plugin],
|
||||
plugin_origins: [{ spec: plugin, scope: "local", source: path.join(tmp.path, "tui.json") }],
|
||||
})
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
|
||||
const api = () =>
|
||||
createTuiPluginApi({
|
||||
@@ -741,7 +794,7 @@ test("updates installed theme when plugin metadata changes", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
|
||||
@@ -4,7 +4,7 @@ import path from "path"
|
||||
import { pathToFileURL } from "url"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { TuiConfig } from "../../../src/config/tui"
|
||||
import { mockTuiService } from "../../fixture/tui-runtime"
|
||||
|
||||
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
|
||||
|
||||
@@ -39,7 +39,7 @@ test("toggles plugin runtime state by exported id", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_enabled: {
|
||||
"demo.toggle": false,
|
||||
@@ -52,7 +52,6 @@ test("toggles plugin runtime state by exported id", async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const api = createTuiPluginApi()
|
||||
|
||||
@@ -85,8 +84,7 @@ test("toggles plugin runtime state by exported id", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
@@ -117,7 +115,7 @@ test("kv plugin_enabled overrides tui config on startup", async () => {
|
||||
})
|
||||
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
const restore = mockTuiService({
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_enabled: {
|
||||
"demo.startup": false,
|
||||
@@ -130,7 +128,6 @@ test("kv plugin_enabled overrides tui config on startup", async () => {
|
||||
},
|
||||
],
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const api = createTuiPluginApi()
|
||||
api.kv.set("plugin_enabled", {
|
||||
@@ -152,8 +149,7 @@ test("kv plugin_enabled overrides tui config on startup", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
|
||||
@@ -8,7 +8,7 @@ import { UI } from "../../../src/cli/ui"
|
||||
import * as Timeout from "../../../src/util/timeout"
|
||||
import * as Network from "../../../src/cli/network"
|
||||
import * as Win32 from "../../../src/cli/cmd/tui/win32"
|
||||
import { TuiConfig } from "../../../src/config/tui"
|
||||
import { mockTuiService } from "../../fixture/tui-runtime"
|
||||
import { Instance } from "../../../src/project/instance"
|
||||
|
||||
const stop = new Error("stop")
|
||||
@@ -42,7 +42,7 @@ function setup() {
|
||||
})
|
||||
spyOn(Win32, "win32DisableProcessedInput").mockImplementation(() => {})
|
||||
spyOn(Win32, "win32InstallCtrlCGuard").mockReturnValue(undefined)
|
||||
spyOn(TuiConfig, "get").mockResolvedValue({})
|
||||
mockTuiService({})
|
||||
spyOn(Instance, "provide").mockImplementation(async (input) => {
|
||||
seen.inst.push(input.directory)
|
||||
return input.fn()
|
||||
|
||||
@@ -13,6 +13,7 @@ const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
|
||||
const wintest = process.platform === "win32" ? test : test.skip
|
||||
const clear = (wait = false) => AppRuntime.runPromise(Config.Service.use((svc) => svc.invalidate(wait)))
|
||||
const load = () => AppRuntime.runPromise(Config.Service.use((svc) => svc.get()))
|
||||
const tuiGet = () => AppRuntime.runPromise(TuiConfig.Service.use((svc) => svc.get()))
|
||||
|
||||
beforeEach(async () => {
|
||||
await clear(true)
|
||||
@@ -83,7 +84,7 @@ test("keeps server and tui plugin merge semantics aligned", async () => {
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const server = await load()
|
||||
const tui = await TuiConfig.get()
|
||||
const tui = await tuiGet()
|
||||
const serverPlugins = (server.plugin ?? []).map((item) => Config.pluginSpecifier(item))
|
||||
const tuiPlugins = (tui.plugin ?? []).map((item) => Config.pluginSpecifier(item))
|
||||
|
||||
@@ -116,7 +117,7 @@ test("loads tui config with the same precedence order as server config paths", a
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("local")
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
},
|
||||
@@ -144,7 +145,7 @@ test("migrates tui-specific keys from opencode.json when tui.json does not exist
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("migrated-theme")
|
||||
expect(config.scroll_speed).toBe(5)
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
@@ -184,7 +185,7 @@ test("migrates project legacy tui keys even when global tui.json already exists"
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("project-migrated")
|
||||
expect(config.scroll_speed).toBe(2)
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
||||
@@ -216,7 +217,7 @@ test("drops unknown legacy tui keys during migration", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("migrated-theme")
|
||||
expect(config.scroll_speed).toBe(2)
|
||||
|
||||
@@ -245,7 +246,7 @@ test("skips migration when opencode.jsonc is syntactically invalid", async () =>
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBeUndefined()
|
||||
expect(config.scroll_speed).toBeUndefined()
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(false)
|
||||
@@ -268,7 +269,7 @@ test("skips migration when tui.json already exists", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
expect(config.theme).toBeUndefined()
|
||||
|
||||
@@ -293,7 +294,7 @@ test("continues loading tui config when legacy source cannot be stripped", async
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("readonly-theme")
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
||||
|
||||
@@ -326,7 +327,7 @@ test("migration backup preserves JSONC comments", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await TuiConfig.get()
|
||||
await tuiGet()
|
||||
const backup = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc.tui-migration.bak"))
|
||||
expect(backup).toContain("// top-level comment")
|
||||
expect(backup).toContain("// nested comment")
|
||||
@@ -349,7 +350,7 @@ test("migrates legacy tui keys across multiple opencode.json levels", async () =
|
||||
await Instance.provide({
|
||||
directory: path.join(tmp.path, "apps", "client"),
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("nested-theme")
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "apps", "client", "tui.json"))).toBe(true)
|
||||
@@ -373,7 +374,7 @@ test("flattens nested tui key inside tui.json", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.scroll_speed).toBe(3)
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
// top-level keys take precedence over nested tui keys
|
||||
@@ -398,7 +399,7 @@ test("top-level keys in tui.json take precedence over nested tui key", async ()
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.diff_style).toBe("auto")
|
||||
expect(config.scroll_speed).toBe(2)
|
||||
},
|
||||
@@ -418,7 +419,7 @@ test("project config takes precedence over OPENCODE_TUI_CONFIG (matches OPENCODE
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
// project tui.json overrides the custom path, same as server config precedence
|
||||
expect(config.theme).toBe("project")
|
||||
// project also set diff_style, so that wins
|
||||
@@ -438,7 +439,7 @@ test("merges keybind overrides across precedence layers", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
expect(config.keybinds?.theme_list).toBe("ctrl+k")
|
||||
},
|
||||
@@ -451,7 +452,7 @@ wintest("defaults Ctrl+Z to input undo on Windows", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
|
||||
},
|
||||
@@ -468,7 +469,7 @@ wintest("keeps explicit input undo overrides on Windows", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+y")
|
||||
},
|
||||
@@ -485,7 +486,7 @@ wintest("ignores terminal suspend bindings on Windows", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
|
||||
},
|
||||
@@ -504,7 +505,7 @@ test("OPENCODE_TUI_CONFIG provides settings when no project config exists", asyn
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("from-env")
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
},
|
||||
@@ -525,7 +526,7 @@ test("does not derive tui path from OPENCODE_CONFIG", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBeUndefined()
|
||||
},
|
||||
})
|
||||
@@ -551,7 +552,7 @@ test("applies env and file substitutions in tui.json", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("env-theme")
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
},
|
||||
@@ -579,7 +580,7 @@ test("applies file substitutions when first identical token is in a commented li
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("resolved-theme")
|
||||
},
|
||||
})
|
||||
@@ -603,7 +604,7 @@ test("loads managed tui config and gives it highest precedence", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("managed-theme")
|
||||
expect(config.plugin).toEqual(["shared-plugin@2.0.0"])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
@@ -628,7 +629,7 @@ test("loads .opencode/tui.json", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
},
|
||||
})
|
||||
@@ -646,7 +647,7 @@ test("gracefully falls back when tui.json has invalid JSON", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.theme).toBe("managed-fallback")
|
||||
expect(config.keybinds).toBeDefined()
|
||||
},
|
||||
@@ -668,7 +669,7 @@ test("supports tuple plugin specs with options in tui.json", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.plugin).toEqual([["acme-plugin@1.2.3", { enabled: true, label: "demo" }]])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
{
|
||||
@@ -705,7 +706,7 @@ test("deduplicates tuple plugin specs by name with higher precedence winning", a
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.plugin).toEqual([
|
||||
["acme-plugin@2.0.0", { source: "project" }],
|
||||
["second-plugin@3.0.0", { source: "project" }],
|
||||
@@ -747,7 +748,7 @@ test("tracks global and local plugin metadata in merged tui config", async () =>
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.plugin).toEqual(["global-plugin@1.0.0", "local-plugin@2.0.0"])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
{
|
||||
@@ -792,7 +793,7 @@ test("merges plugin_enabled flags across config layers", async () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
const config = await tuiGet()
|
||||
expect(config.plugin_enabled).toEqual({
|
||||
"internal:sidebar-context": false,
|
||||
"demo.plugin": false,
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import { spyOn } from "bun:test"
|
||||
import path from "path"
|
||||
import { Effect } from "effect"
|
||||
import { TuiConfig } from "../../src/config/tui"
|
||||
|
||||
type PluginSpec = string | [string, Record<string, unknown>]
|
||||
|
||||
/**
|
||||
* Mock `TuiConfig.Service.use` so callers that do
|
||||
* `AppRuntime.runPromise(TuiConfig.Service.use(svc => svc.get()))` receive
|
||||
* the provided config object instead of loading from disk.
|
||||
*
|
||||
* Returns a restore function.
|
||||
*/
|
||||
export function mockTuiService(config: TuiConfig.Info, opts?: { wait?: () => Effect.Effect<void> }) {
|
||||
const mock: TuiConfig.Interface = {
|
||||
get: () => Effect.succeed(config),
|
||||
waitForDependencies: () => opts?.wait?.() ?? Effect.void,
|
||||
}
|
||||
const spy = spyOn(TuiConfig.Service, "use" as never).mockImplementation(((fn: (svc: TuiConfig.Interface) => any) =>
|
||||
fn(mock)) as never)
|
||||
return () => spy.mockRestore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Full mock: sets OPENCODE_PLUGIN_META_FILE, mocks cwd, and mocks
|
||||
* TuiConfig.Service with the given plugins.
|
||||
*/
|
||||
export function mockTuiRuntime(dir: string, plugin: PluginSpec[]) {
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(dir, "plugin-meta.json")
|
||||
const plugin_origins = plugin.map((spec) => ({
|
||||
@@ -11,17 +33,12 @@ export function mockTuiRuntime(dir: string, plugin: PluginSpec[]) {
|
||||
scope: "local" as const,
|
||||
source: path.join(dir, "tui.json"),
|
||||
}))
|
||||
const get = spyOn(TuiConfig, "get").mockResolvedValue({
|
||||
plugin,
|
||||
plugin_origins,
|
||||
})
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const restore = mockTuiService({ plugin, plugin_origins })
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => dir)
|
||||
|
||||
return () => {
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
restore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user