CLI perf: reduce deps (#22652)
This commit is contained in:
@@ -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"
|
||||
import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
|
||||
|
||||
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
|
||||
|
||||
@@ -31,15 +31,18 @@ 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 config: TuiConfig.Info = {
|
||||
plugin: [],
|
||||
plugin_origins: undefined,
|
||||
})
|
||||
}
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({
|
||||
api: createTuiPluginApi(),
|
||||
config,
|
||||
})
|
||||
|
||||
await expect(TuiPluginRuntime.addPlugin(tmp.extra.spec)).resolves.toBe(true)
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("called")
|
||||
@@ -54,7 +57,6 @@ test("adds tui plugin at runtime from spec", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
@@ -72,10 +74,10 @@ 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({
|
||||
const config: TuiConfig.Info = {
|
||||
plugin: [],
|
||||
plugin_origins: undefined,
|
||||
})
|
||||
}
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockImplementation(async () => {
|
||||
await Bun.write(
|
||||
path.join(tmp.extra.mod, "index.ts"),
|
||||
@@ -91,7 +93,10 @@ test("retries runtime add for file plugins after dependency wait", async () => {
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({
|
||||
api: createTuiPluginApi(),
|
||||
config,
|
||||
})
|
||||
|
||||
await expect(TuiPluginRuntime.addPlugin(tmp.extra.spec)).resolves.toBe(true)
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("called")
|
||||
@@ -100,7 +105,6 @@ test("retries runtime add for file plugins after dependency wait", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
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"
|
||||
import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
|
||||
|
||||
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
|
||||
|
||||
@@ -50,11 +50,10 @@ 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 config: TuiConfig.Info = {
|
||||
plugin: [],
|
||||
plugin_origins: undefined,
|
||||
}
|
||||
const get = spyOn(TuiConfig, "get").mockImplementation(async () => cfg)
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const api = createTuiPluginApi({
|
||||
@@ -69,7 +68,7 @@ test("installs plugin without loading it", async () => {
|
||||
})
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(api)
|
||||
await TuiPluginRuntime.init({ api, config })
|
||||
const out = await TuiPluginRuntime.installPlugin(tmp.extra.spec)
|
||||
expect(out).toMatchObject({
|
||||
ok: true,
|
||||
@@ -82,7 +81,6 @@ test("installs plugin without loading it", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
|
||||
@@ -39,10 +39,10 @@ test("runs onDispose callbacks with aborted signal and is idempotent", async ()
|
||||
},
|
||||
})
|
||||
|
||||
const restore = mockTuiRuntime(tmp.path, [[tmp.extra.spec, { marker: tmp.extra.marker }]])
|
||||
const { config, restore } = mockTuiRuntime(tmp.path, [[tmp.extra.spec, { marker: tmp.extra.marker }]])
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
await TuiPluginRuntime.dispose()
|
||||
|
||||
const marker = await fs.readFile(tmp.extra.marker, "utf8")
|
||||
@@ -99,13 +99,13 @@ test("rolls back failed plugin and continues loading next", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
const restore = mockTuiRuntime(tmp.path, [
|
||||
const { config, restore } = mockTuiRuntime(tmp.path, [
|
||||
[tmp.extra.badSpec, { bad_marker: tmp.extra.badMarker }],
|
||||
[tmp.extra.goodSpec, { good_marker: tmp.extra.goodMarker }],
|
||||
])
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
// bad plugin's onDispose ran during rollback
|
||||
await expect(fs.readFile(tmp.extra.badMarker, "utf8")).resolves.toBe("cleaned")
|
||||
// good plugin still loaded
|
||||
@@ -155,11 +155,11 @@ export default {
|
||||
},
|
||||
})
|
||||
|
||||
const restore = mockTuiRuntime(tmp.path, [tmp.extra.spec])
|
||||
const { config, restore } = mockTuiRuntime(tmp.path, [tmp.extra.spec])
|
||||
const err = spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
|
||||
const marker = await fs.readFile(tmp.extra.marker, "utf8")
|
||||
expect(marker).toContain("one")
|
||||
@@ -202,10 +202,10 @@ test(
|
||||
},
|
||||
})
|
||||
|
||||
const restore = mockTuiRuntime(tmp.path, [tmp.extra.spec])
|
||||
const { config, restore } = mockTuiRuntime(tmp.path, [tmp.extra.spec])
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
|
||||
const done = await new Promise<string>((resolve) => {
|
||||
const timer = setTimeout(() => resolve("timeout"), 7000)
|
||||
|
||||
@@ -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"
|
||||
import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
|
||||
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 config: TuiConfig.Info = {
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -53,13 +53,13 @@ test("loads npm tui plugin from package ./tui export", async () => {
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
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 })
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("called")
|
||||
const hit = TuiPluginRuntime.list().find((item) => item.id === "demo.tui.export")
|
||||
expect(hit?.enabled).toBe(true)
|
||||
@@ -69,7 +69,6 @@ test("loads npm tui plugin from package ./tui export", async () => {
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
@@ -106,7 +105,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 config: TuiConfig.Info = {
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -115,20 +114,19 @@ test("does not use npm package exports dot for tui entry", async () => {
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
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 })
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).rejects.toThrow()
|
||||
expect(TuiPluginRuntime.list().some((item) => item.spec === tmp.extra.spec)).toBe(false)
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
@@ -169,7 +167,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 config: TuiConfig.Info = {
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -178,13 +176,13 @@ test("rejects npm tui export that resolves outside plugin directory", async () =
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
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 })
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
// plugin code never ran
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).rejects.toThrow()
|
||||
// plugin not listed
|
||||
@@ -193,7 +191,6 @@ test("rejects npm tui export that resolves outside plugin directory", async () =
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
@@ -232,7 +229,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 config: TuiConfig.Info = {
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -241,20 +238,19 @@ test("rejects npm tui plugin that exports server and tui together", async () =>
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
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 })
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).rejects.toThrow()
|
||||
expect(TuiPluginRuntime.list().some((item) => item.spec === tmp.extra.spec)).toBe(false)
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
@@ -291,7 +287,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 config: TuiConfig.Info = {
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -300,7 +296,7 @@ test("does not use npm package main for tui entry", async () => {
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
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 })
|
||||
@@ -308,7 +304,7 @@ test("does not use npm package main for tui entry", async () => {
|
||||
const error = spyOn(console, "error").mockImplementation(() => {})
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).rejects.toThrow()
|
||||
expect(TuiPluginRuntime.list().some((item) => item.spec === tmp.extra.spec)).toBe(false)
|
||||
expect(error).not.toHaveBeenCalled()
|
||||
@@ -317,7 +313,6 @@ test("does not use npm package main for tui entry", async () => {
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
warn.mockRestore()
|
||||
error.mockRestore()
|
||||
@@ -357,7 +352,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 config: TuiConfig.Info = {
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -366,18 +361,17 @@ test("does not use directory package main for tui entry", async () => {
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).rejects.toThrow()
|
||||
expect(TuiPluginRuntime.list().some((item) => item.spec === tmp.extra.spec)).toBe(false)
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
@@ -405,7 +399,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 config: TuiConfig.Info = {
|
||||
plugin: [tmp.extra.spec],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -414,18 +408,17 @@ test("uses directory index fallback for tui when package.json is missing", async
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("called")
|
||||
expect(TuiPluginRuntime.list().find((item) => item.id === "demo.dir.index")?.active).toBe(true)
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
@@ -463,7 +456,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 config: TuiConfig.Info = {
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -472,20 +465,19 @@ test("uses npm package name when tui plugin id is omitted", async () => {
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
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 })
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("called")
|
||||
expect(TuiPluginRuntime.list().find((item) => item.spec === tmp.extra.spec)?.id).toBe("acme-plugin")
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
install.mockRestore()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
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"
|
||||
import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
|
||||
|
||||
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 config: TuiConfig.Info = {
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_origins: [
|
||||
{
|
||||
@@ -46,17 +46,16 @@ test("skips external tui plugins in pure mode", async () => {
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).rejects.toThrow()
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
if (pure === undefined) {
|
||||
delete process.env.OPENCODE_PURE
|
||||
|
||||
@@ -5,8 +5,8 @@ import { pathToFileURL } from "url"
|
||||
import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { Global } from "../../../src/global"
|
||||
import { TuiConfig } from "../../../src/config"
|
||||
import { Filesystem } from "../../../src/util"
|
||||
import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
|
||||
import { Filesystem } from "../../../src/util/"
|
||||
|
||||
const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme")
|
||||
const { TuiPluginRuntime } = await import("../../../src/cli/cmd/tui/plugin/runtime")
|
||||
@@ -328,8 +328,55 @@ export default {
|
||||
try {
|
||||
expect(addTheme(tmp.extra.preloadedThemeName, { theme: { primary: "#303030" } })).toBe(true)
|
||||
|
||||
await TuiPluginRuntime.init(
|
||||
createTuiPluginApi({
|
||||
const localOpts = {
|
||||
fn_marker: tmp.extra.fnMarker,
|
||||
marker: tmp.extra.localMarker,
|
||||
source: tmp.extra.localDest.replace(".opencode/themes/", ""),
|
||||
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" },
|
||||
}
|
||||
const invalidOpts = {
|
||||
marker: tmp.extra.invalidMarker,
|
||||
theme_path: `./${tmp.extra.invalidThemeFile}`,
|
||||
theme_name: tmp.extra.invalidThemeName,
|
||||
}
|
||||
const preloadedOpts = {
|
||||
marker: tmp.extra.preloadedMarker,
|
||||
dest: tmp.extra.preloadedDest,
|
||||
theme_path: `./${tmp.extra.preloadedThemeFile}`,
|
||||
theme_name: tmp.extra.preloadedThemeName,
|
||||
}
|
||||
const globalOpts = {
|
||||
marker: tmp.extra.globalMarker,
|
||||
theme_path: `./${tmp.extra.globalThemeFile}`,
|
||||
theme_name: tmp.extra.globalThemeName,
|
||||
}
|
||||
|
||||
const config: TuiConfig.Info = {
|
||||
plugin: [
|
||||
[tmp.extra.localSpec, localOpts],
|
||||
[tmp.extra.invalidSpec, invalidOpts],
|
||||
[tmp.extra.preloadedSpec, preloadedOpts],
|
||||
[tmp.extra.globalSpec, globalOpts],
|
||||
],
|
||||
plugin_origins: [
|
||||
{ spec: [tmp.extra.localSpec, localOpts], scope: "local", source: path.join(tmp.path, "tui.json") },
|
||||
{ spec: [tmp.extra.invalidSpec, invalidOpts], scope: "local", source: path.join(tmp.path, "tui.json") },
|
||||
{ spec: [tmp.extra.preloadedSpec, preloadedOpts], scope: "local", source: path.join(tmp.path, "tui.json") },
|
||||
{
|
||||
spec: [tmp.extra.globalSpec, globalOpts],
|
||||
scope: "global",
|
||||
source: path.join(Global.Path.config, "tui.json"),
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
await TuiPluginRuntime.init({
|
||||
api: createTuiPluginApi({
|
||||
tuiConfig: {
|
||||
theme: "smoke",
|
||||
diff_style: "stacked",
|
||||
@@ -366,7 +413,8 @@ export default {
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
config,
|
||||
})
|
||||
const local = await row(tmp.extra.localMarker)
|
||||
const global = await row(tmp.extra.globalMarker)
|
||||
const invalid = await row(tmp.extra.invalidMarker)
|
||||
@@ -459,7 +507,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 config: TuiConfig.Info = {
|
||||
plugin: [
|
||||
[tmp.extra.badSpec, { marker: path.join(tmp.path, "bad.txt") }],
|
||||
[tmp.extra.goodSpec, { marker: tmp.extra.goodMarker }],
|
||||
@@ -477,12 +525,12 @@ test("continues loading when a plugin is missing config metadata", async () => {
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
// bad plugin was skipped (no metadata entry)
|
||||
await expect(fs.readFile(path.join(tmp.path, "bad.txt"), "utf8")).rejects.toThrow()
|
||||
// good plugin loaded fine
|
||||
@@ -492,7 +540,6 @@ test("continues loading when a plugin is missing config metadata", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
@@ -555,7 +602,18 @@ export default {
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(createTuiPluginApi())
|
||||
const a = path.join(tmp.path, "order-a.ts")
|
||||
const b = path.join(tmp.path, "order-b.ts")
|
||||
const aSpec = pathToFileURL(a).href
|
||||
const bSpec = pathToFileURL(b).href
|
||||
const config: TuiConfig.Info = {
|
||||
plugin: [aSpec, bSpec],
|
||||
plugin_origins: [
|
||||
{ spec: aSpec, scope: "local", source: path.join(tmp.path, "tui.json") },
|
||||
{ spec: bSpec, scope: "local", source: path.join(tmp.path, "tui.json") },
|
||||
],
|
||||
}
|
||||
await TuiPluginRuntime.init({ api: createTuiPluginApi(), config })
|
||||
const lines = (await fs.readFile(tmp.extra.marker, "utf8")).trim().split("\n")
|
||||
expect(lines).toEqual(["a-start", "a-end", "b"])
|
||||
} finally {
|
||||
@@ -699,7 +757,7 @@ test("updates installed theme when plugin metadata changes", async () => {
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
|
||||
const api = () =>
|
||||
const mkApi = () =>
|
||||
createTuiPluginApi({
|
||||
theme: {
|
||||
has(name) {
|
||||
@@ -708,8 +766,19 @@ test("updates installed theme when plugin metadata changes", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
const mkConfig = (): TuiConfig.Info => ({
|
||||
plugin: [[tmp.extra.spec, { theme_path: `./theme-update.json` }]],
|
||||
plugin_origins: [
|
||||
{
|
||||
spec: [tmp.extra.spec, { theme_path: `./theme-update.json` }],
|
||||
scope: "local",
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(api())
|
||||
await TuiPluginRuntime.init({ api: mkApi(), config: mkConfig() })
|
||||
await TuiPluginRuntime.dispose()
|
||||
await expect(fs.readFile(tmp.extra.dest, "utf8")).resolves.toContain("#111111")
|
||||
|
||||
@@ -730,7 +799,7 @@ test("updates installed theme when plugin metadata changes", async () => {
|
||||
await fs.utimes(tmp.extra.pluginPath, stamp, stamp)
|
||||
await fs.utimes(tmp.extra.themePath, stamp, stamp)
|
||||
|
||||
await TuiPluginRuntime.init(api())
|
||||
await TuiPluginRuntime.init({ api: mkApi(), config: mkConfig() })
|
||||
const text = await fs.readFile(tmp.extra.dest, "utf8")
|
||||
expect(text).toContain("#222222")
|
||||
expect(text).not.toContain("#111111")
|
||||
|
||||
@@ -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"
|
||||
import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
|
||||
|
||||
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 config: TuiConfig.Info = {
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_enabled: {
|
||||
"demo.toggle": false,
|
||||
@@ -51,13 +51,13 @@ test("toggles plugin runtime state by exported id", async () => {
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const api = createTuiPluginApi()
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(api)
|
||||
await TuiPluginRuntime.init({ api, config })
|
||||
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).rejects.toThrow()
|
||||
expect(TuiPluginRuntime.list().find((item) => item.id === "demo.toggle")).toEqual({
|
||||
@@ -85,7 +85,6 @@ test("toggles plugin runtime state by exported id", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
@@ -117,7 +116,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 config: TuiConfig.Info = {
|
||||
plugin: [[tmp.extra.spec, { marker: tmp.extra.marker }]],
|
||||
plugin_enabled: {
|
||||
"demo.startup": false,
|
||||
@@ -129,7 +128,7 @@ test("kv plugin_enabled overrides tui config on startup", async () => {
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const api = createTuiPluginApi()
|
||||
@@ -138,7 +137,7 @@ test("kv plugin_enabled overrides tui config on startup", async () => {
|
||||
})
|
||||
|
||||
try {
|
||||
await TuiPluginRuntime.init(api)
|
||||
await TuiPluginRuntime.init({ api, config })
|
||||
|
||||
await expect(fs.readFile(tmp.extra.marker, "utf8")).resolves.toBe("on")
|
||||
expect(TuiPluginRuntime.list().find((item) => item.id === "demo.startup")).toEqual({
|
||||
@@ -152,7 +151,6 @@ test("kv plugin_enabled overrides tui config on startup", async () => {
|
||||
} finally {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
|
||||
@@ -8,13 +8,11 @@ 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"
|
||||
import { Instance } from "../../../src/project/instance"
|
||||
import { TuiConfig } from "../../../src/cli/cmd/tui/config/tui"
|
||||
|
||||
const stop = new Error("stop")
|
||||
const seen = {
|
||||
tui: [] as string[],
|
||||
inst: [] as string[],
|
||||
}
|
||||
|
||||
function setup() {
|
||||
@@ -42,11 +40,6 @@ function setup() {
|
||||
})
|
||||
spyOn(Win32, "win32DisableProcessedInput").mockImplementation(() => {})
|
||||
spyOn(Win32, "win32InstallCtrlCGuard").mockReturnValue(undefined)
|
||||
spyOn(TuiConfig, "get").mockResolvedValue({})
|
||||
spyOn(Instance, "provide").mockImplementation(async (input) => {
|
||||
seen.inst.push(input.directory)
|
||||
return input.fn()
|
||||
})
|
||||
}
|
||||
|
||||
describe("tui thread", () => {
|
||||
@@ -86,7 +79,6 @@ describe("tui thread", () => {
|
||||
const link = path.join(path.dirname(tmp.path), path.basename(tmp.path) + "-link")
|
||||
const type = process.platform === "win32" ? "junction" : "dir"
|
||||
seen.tui.length = 0
|
||||
seen.inst.length = 0
|
||||
await fs.symlink(tmp.path, link, type)
|
||||
|
||||
Object.defineProperty(process.stdin, "isTTY", {
|
||||
@@ -105,7 +97,6 @@ describe("tui thread", () => {
|
||||
process.chdir(tmp.path)
|
||||
process.env.PWD = link
|
||||
await expect(call(project)).rejects.toBe(stop)
|
||||
expect(seen.inst[0]).toBe(tmp.path)
|
||||
expect(seen.tui[0]).toBe(tmp.path)
|
||||
} finally {
|
||||
process.chdir(cwd)
|
||||
|
||||
@@ -26,6 +26,7 @@ import { ProjectID } from "../../src/project/schema"
|
||||
import { Filesystem } from "../../src/util"
|
||||
import * as Network from "../../src/util/network"
|
||||
import { Npm } from "../../src/npm"
|
||||
import { ConfigPlugin } from "@/config/plugin"
|
||||
|
||||
const emptyAccount = Layer.mock(Account.Service)({
|
||||
active: () => Effect.succeed(Option.none()),
|
||||
@@ -1256,7 +1257,7 @@ test("keeps plugin origins aligned with merged plugin list", async () => {
|
||||
const cfg = await load()
|
||||
const plugins = cfg.plugin ?? []
|
||||
const origins = cfg.plugin_origins ?? []
|
||||
const names = plugins.map((item) => Config.pluginSpecifier(item))
|
||||
const names = plugins.map((item) => ConfigPlugin.pluginSpecifier(item))
|
||||
|
||||
expect(names).toContain("shared-plugin@2.0.0")
|
||||
expect(names).not.toContain("shared-plugin@1.0.0")
|
||||
@@ -1264,7 +1265,7 @@ test("keeps plugin origins aligned with merged plugin list", async () => {
|
||||
expect(names).toContain("local-only@1.0.0")
|
||||
|
||||
expect(origins.map((item) => item.spec)).toEqual(plugins)
|
||||
const hit = origins.find((item) => Config.pluginSpecifier(item.spec) === "shared-plugin@2.0.0")
|
||||
const hit = origins.find((item) => ConfigPlugin.pluginSpecifier(item.spec) === "shared-plugin@2.0.0")
|
||||
expect(hit?.scope).toBe("local")
|
||||
},
|
||||
})
|
||||
@@ -1909,8 +1910,8 @@ describe("resolvePluginSpec", () => {
|
||||
test("keeps package specs unchanged", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const file = path.join(tmp.path, "opencode.json")
|
||||
expect(await Config.resolvePluginSpec("oh-my-opencode@2.4.3", file)).toBe("oh-my-opencode@2.4.3")
|
||||
expect(await Config.resolvePluginSpec("@scope/pkg", file)).toBe("@scope/pkg")
|
||||
expect(await ConfigPlugin.resolvePluginSpec("oh-my-opencode@2.4.3", file)).toBe("oh-my-opencode@2.4.3")
|
||||
expect(await ConfigPlugin.resolvePluginSpec("@scope/pkg", file)).toBe("@scope/pkg")
|
||||
})
|
||||
|
||||
test("resolves windows-style relative plugin directory specs", async () => {
|
||||
@@ -1925,8 +1926,8 @@ describe("resolvePluginSpec", () => {
|
||||
})
|
||||
|
||||
const file = path.join(tmp.path, "opencode.json")
|
||||
const hit = await Config.resolvePluginSpec(".\\plugin", file)
|
||||
expect(Config.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href)
|
||||
const hit = await ConfigPlugin.resolvePluginSpec(".\\plugin", file)
|
||||
expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin", "index.ts")).href)
|
||||
})
|
||||
|
||||
test("resolves relative file plugin paths to file urls", async () => {
|
||||
@@ -1937,8 +1938,8 @@ describe("resolvePluginSpec", () => {
|
||||
})
|
||||
|
||||
const file = path.join(tmp.path, "opencode.json")
|
||||
const hit = await Config.resolvePluginSpec("./plugin.ts", file)
|
||||
expect(Config.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin.ts")).href)
|
||||
const hit = await ConfigPlugin.resolvePluginSpec("./plugin.ts", file)
|
||||
expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin.ts")).href)
|
||||
})
|
||||
|
||||
test("resolves plugin directory paths to directory urls", async () => {
|
||||
@@ -1956,8 +1957,8 @@ describe("resolvePluginSpec", () => {
|
||||
})
|
||||
|
||||
const file = path.join(tmp.path, "opencode.json")
|
||||
const hit = await Config.resolvePluginSpec("./plugin", file)
|
||||
expect(Config.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin")).href)
|
||||
const hit = await ConfigPlugin.resolvePluginSpec("./plugin", file)
|
||||
expect(ConfigPlugin.pluginSpecifier(hit)).toBe(pathToFileURL(path.join(tmp.path, "plugin")).href)
|
||||
})
|
||||
|
||||
test("resolves plugin directories without package.json to index.ts", async () => {
|
||||
|
||||
0
packages/opencode/test/config/plugin.test.ts
Normal file
0
packages/opencode/test/config/plugin.test.ts
Normal file
@@ -3,13 +3,15 @@ import path from "path"
|
||||
import fs from "fs/promises"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
import { TuiConfig } from "../../src/cli/cmd/tui/config/tui"
|
||||
import { Config } from "../../src/config"
|
||||
import { TuiConfig } from "../../src/config"
|
||||
import { Global } from "../../src/global"
|
||||
import { Filesystem } from "../../src/util"
|
||||
import { AppRuntime } from "../../src/effect/app-runtime"
|
||||
import { Effect, Layer } from "effect"
|
||||
import { CurrentWorkingDirectory } from "@/cli/cmd/tui/config/cwd"
|
||||
import { ConfigPlugin } from "@/config/plugin"
|
||||
|
||||
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()))
|
||||
@@ -18,6 +20,13 @@ beforeEach(async () => {
|
||||
await clear(true)
|
||||
})
|
||||
|
||||
const getTuiConfig = async (directory: string) =>
|
||||
Effect.runPromise(
|
||||
TuiConfig.Service.use((svc) => svc.get()).pipe(
|
||||
Effect.provide(TuiConfig.defaultLayer.pipe(Layer.provide(Layer.succeed(CurrentWorkingDirectory, directory)))),
|
||||
),
|
||||
)
|
||||
|
||||
afterEach(async () => {
|
||||
delete process.env.OPENCODE_CONFIG
|
||||
delete process.env.OPENCODE_TUI_CONFIG
|
||||
@@ -25,7 +34,6 @@ afterEach(async () => {
|
||||
await fs.rm(path.join(Global.Path.config, "opencode.jsonc"), { force: true }).catch(() => {})
|
||||
await fs.rm(path.join(Global.Path.config, "tui.json"), { force: true }).catch(() => {})
|
||||
await fs.rm(path.join(Global.Path.config, "tui.jsonc"), { force: true }).catch(() => {})
|
||||
await fs.rm(managedConfigDir, { force: true, recursive: true }).catch(() => {})
|
||||
await clear(true)
|
||||
})
|
||||
|
||||
@@ -83,9 +91,9 @@ 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 serverPlugins = (server.plugin ?? []).map((item) => Config.pluginSpecifier(item))
|
||||
const tuiPlugins = (tui.plugin ?? []).map((item) => Config.pluginSpecifier(item))
|
||||
const tui = await getTuiConfig(tmp.path)
|
||||
const serverPlugins = (server.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item))
|
||||
const tuiPlugins = (tui.plugin ?? []).map((item) => ConfigPlugin.pluginSpecifier(item))
|
||||
|
||||
expect(serverPlugins).toEqual(tuiPlugins)
|
||||
expect(serverPlugins).toContain("shared-plugin@2.0.0")
|
||||
@@ -93,8 +101,8 @@ test("keeps server and tui plugin merge semantics aligned", async () => {
|
||||
|
||||
const serverOrigins = server.plugin_origins ?? []
|
||||
const tuiOrigins = tui.plugin_origins ?? []
|
||||
expect(serverOrigins.map((item) => Config.pluginSpecifier(item.spec))).toEqual(serverPlugins)
|
||||
expect(tuiOrigins.map((item) => Config.pluginSpecifier(item.spec))).toEqual(tuiPlugins)
|
||||
expect(serverOrigins.map((item) => ConfigPlugin.pluginSpecifier(item.spec))).toEqual(serverPlugins)
|
||||
expect(tuiOrigins.map((item) => ConfigPlugin.pluginSpecifier(item.spec))).toEqual(tuiPlugins)
|
||||
expect(serverOrigins.map((item) => item.scope)).toEqual(tuiOrigins.map((item) => item.scope))
|
||||
},
|
||||
})
|
||||
@@ -113,14 +121,9 @@ 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()
|
||||
expect(config.theme).toBe("local")
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("local")
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
})
|
||||
|
||||
test("migrates tui-specific keys from opencode.json when tui.json does not exist", async () => {
|
||||
@@ -141,26 +144,21 @@ 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()
|
||||
expect(config.theme).toBe("migrated-theme")
|
||||
expect(config.scroll_speed).toBe(5)
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
const text = await Filesystem.readText(path.join(tmp.path, "tui.json"))
|
||||
expect(JSON.parse(text)).toMatchObject({
|
||||
theme: "migrated-theme",
|
||||
scroll_speed: 5,
|
||||
})
|
||||
const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
|
||||
expect(server.theme).toBeUndefined()
|
||||
expect(server.keybinds).toBeUndefined()
|
||||
expect(server.tui).toBeUndefined()
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(true)
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
||||
},
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("migrated-theme")
|
||||
expect(config.scroll_speed).toBe(5)
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
const text = await Filesystem.readText(path.join(tmp.path, "tui.json"))
|
||||
expect(JSON.parse(text)).toMatchObject({
|
||||
theme: "migrated-theme",
|
||||
scroll_speed: 5,
|
||||
})
|
||||
const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
|
||||
expect(server.theme).toBeUndefined()
|
||||
expect(server.keybinds).toBeUndefined()
|
||||
expect(server.tui).toBeUndefined()
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(true)
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
||||
})
|
||||
|
||||
test("migrates project legacy tui keys even when global tui.json already exists", async () => {
|
||||
@@ -181,19 +179,14 @@ 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()
|
||||
expect(config.theme).toBe("project-migrated")
|
||||
expect(config.scroll_speed).toBe(2)
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("project-migrated")
|
||||
expect(config.scroll_speed).toBe(2)
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
||||
|
||||
const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
|
||||
expect(server.theme).toBeUndefined()
|
||||
expect(server.tui).toBeUndefined()
|
||||
},
|
||||
})
|
||||
const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
|
||||
expect(server.theme).toBeUndefined()
|
||||
expect(server.tui).toBeUndefined()
|
||||
})
|
||||
|
||||
test("drops unknown legacy tui keys during migration", async () => {
|
||||
@@ -213,19 +206,14 @@ test("drops unknown legacy tui keys during migration", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.theme).toBe("migrated-theme")
|
||||
expect(config.scroll_speed).toBe(2)
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("migrated-theme")
|
||||
expect(config.scroll_speed).toBe(2)
|
||||
|
||||
const text = await Filesystem.readText(path.join(tmp.path, "tui.json"))
|
||||
const migrated = JSON.parse(text)
|
||||
expect(migrated.scroll_speed).toBe(2)
|
||||
expect(migrated.foo).toBeUndefined()
|
||||
},
|
||||
})
|
||||
const text = await Filesystem.readText(path.join(tmp.path, "tui.json"))
|
||||
const migrated = JSON.parse(text)
|
||||
expect(migrated.scroll_speed).toBe(2)
|
||||
expect(migrated.foo).toBeUndefined()
|
||||
})
|
||||
|
||||
test("skips migration when opencode.jsonc is syntactically invalid", async () => {
|
||||
@@ -242,19 +230,14 @@ test("skips migration when opencode.jsonc is syntactically invalid", async () =>
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.theme).toBeUndefined()
|
||||
expect(config.scroll_speed).toBeUndefined()
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(false)
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "opencode.jsonc.tui-migration.bak"))).toBe(false)
|
||||
const source = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc"))
|
||||
expect(source).toContain('"theme": "broken-theme"')
|
||||
expect(source).toContain('"tui": { "scroll_speed": 2 }')
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBeUndefined()
|
||||
expect(config.scroll_speed).toBeUndefined()
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(false)
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "opencode.jsonc.tui-migration.bak"))).toBe(false)
|
||||
const source = await Filesystem.readText(path.join(tmp.path, "opencode.jsonc"))
|
||||
expect(source).toContain('"theme": "broken-theme"')
|
||||
expect(source).toContain('"tui": { "scroll_speed": 2 }')
|
||||
})
|
||||
|
||||
test("skips migration when tui.json already exists", async () => {
|
||||
@@ -265,18 +248,13 @@ test("skips migration when tui.json already exists", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
expect(config.theme).toBeUndefined()
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
expect(config.theme).toBeUndefined()
|
||||
|
||||
const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
|
||||
expect(server.theme).toBe("legacy")
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(false)
|
||||
},
|
||||
})
|
||||
const server = JSON.parse(await Filesystem.readText(path.join(tmp.path, "opencode.json")))
|
||||
expect(server.theme).toBe("legacy")
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "opencode.json.tui-migration.bak"))).toBe(false)
|
||||
})
|
||||
|
||||
test("continues loading tui config when legacy source cannot be stripped", async () => {
|
||||
@@ -290,17 +268,12 @@ test("continues loading tui config when legacy source cannot be stripped", async
|
||||
await fs.chmod(source, 0o444)
|
||||
|
||||
try {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.theme).toBe("readonly-theme")
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("readonly-theme")
|
||||
expect(await Filesystem.exists(path.join(tmp.path, "tui.json"))).toBe(true)
|
||||
|
||||
const server = JSON.parse(await Filesystem.readText(source))
|
||||
expect(server.theme).toBe("readonly-theme")
|
||||
},
|
||||
})
|
||||
const server = JSON.parse(await Filesystem.readText(source))
|
||||
expect(server.theme).toBe("readonly-theme")
|
||||
} finally {
|
||||
await fs.chmod(source, 0o644)
|
||||
}
|
||||
@@ -323,17 +296,12 @@ test("migration backup preserves JSONC comments", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
await TuiConfig.get()
|
||||
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")
|
||||
expect(backup).toContain('"theme": "jsonc-theme"')
|
||||
expect(backup).toContain('"scroll_speed": 1.5')
|
||||
},
|
||||
})
|
||||
await getTuiConfig(tmp.path)
|
||||
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")
|
||||
expect(backup).toContain('"theme": "jsonc-theme"')
|
||||
expect(backup).toContain('"scroll_speed": 1.5')
|
||||
})
|
||||
|
||||
test("migrates legacy tui keys across multiple opencode.json levels", async () => {
|
||||
@@ -345,16 +313,10 @@ test("migrates legacy tui keys across multiple opencode.json levels", async () =
|
||||
await Bun.write(path.join(nested, "opencode.json"), JSON.stringify({ theme: "nested-theme" }, null, 2))
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: path.join(tmp.path, "apps", "client"),
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
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)
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(path.join(tmp.path, "apps", "client"))
|
||||
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)
|
||||
})
|
||||
|
||||
test("flattens nested tui key inside tui.json", async () => {
|
||||
@@ -370,16 +332,11 @@ test("flattens nested tui key inside tui.json", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.scroll_speed).toBe(3)
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
// top-level keys take precedence over nested tui keys
|
||||
expect(config.theme).toBe("outer")
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.scroll_speed).toBe(3)
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
// top-level keys take precedence over nested tui keys
|
||||
expect(config.theme).toBe("outer")
|
||||
})
|
||||
|
||||
test("top-level keys in tui.json take precedence over nested tui key", async () => {
|
||||
@@ -395,14 +352,9 @@ 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()
|
||||
expect(config.diff_style).toBe("auto")
|
||||
expect(config.scroll_speed).toBe(2)
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.diff_style).toBe("auto")
|
||||
expect(config.scroll_speed).toBe(2)
|
||||
})
|
||||
|
||||
test("project config takes precedence over OPENCODE_TUI_CONFIG (matches OPENCODE_CONFIG)", async () => {
|
||||
@@ -415,16 +367,11 @@ test("project config takes precedence over OPENCODE_TUI_CONFIG (matches OPENCODE
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
// 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
|
||||
expect(config.diff_style).toBe("auto")
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
// 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
|
||||
expect(config.diff_style).toBe("auto")
|
||||
})
|
||||
|
||||
test("merges keybind overrides across precedence layers", async () => {
|
||||
@@ -434,28 +381,16 @@ test("merges keybind overrides across precedence layers", async () => {
|
||||
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ keybinds: { theme_list: "ctrl+k" } }))
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
expect(config.keybinds?.theme_list).toBe("ctrl+k")
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
expect(config.keybinds?.theme_list).toBe("ctrl+k")
|
||||
})
|
||||
|
||||
wintest("defaults Ctrl+Z to input undo on Windows", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
|
||||
})
|
||||
|
||||
wintest("keeps explicit input undo overrides on Windows", async () => {
|
||||
@@ -464,15 +399,9 @@ wintest("keeps explicit input undo overrides on Windows", async () => {
|
||||
await Bun.write(path.join(dir, "tui.json"), JSON.stringify({ keybinds: { input_undo: "ctrl+y" } }))
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+y")
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+y")
|
||||
})
|
||||
|
||||
wintest("ignores terminal suspend bindings on Windows", async () => {
|
||||
@@ -482,14 +411,9 @@ wintest("ignores terminal suspend bindings on Windows", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.keybinds?.terminal_suspend).toBe("none")
|
||||
expect(config.keybinds?.input_undo).toBe("ctrl+z,ctrl+-,super+z")
|
||||
})
|
||||
|
||||
test("OPENCODE_TUI_CONFIG provides settings when no project config exists", async () => {
|
||||
@@ -500,15 +424,9 @@ test("OPENCODE_TUI_CONFIG provides settings when no project config exists", asyn
|
||||
process.env.OPENCODE_TUI_CONFIG = custom
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.theme).toBe("from-env")
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("from-env")
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
})
|
||||
|
||||
test("does not derive tui path from OPENCODE_CONFIG", async () => {
|
||||
@@ -521,14 +439,8 @@ test("does not derive tui path from OPENCODE_CONFIG", async () => {
|
||||
process.env.OPENCODE_CONFIG = path.join(customDir, "opencode.json")
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.theme).toBeUndefined()
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBeUndefined()
|
||||
})
|
||||
|
||||
test("applies env and file substitutions in tui.json", async () => {
|
||||
@@ -547,15 +459,9 @@ test("applies env and file substitutions in tui.json", async () => {
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.theme).toBe("env-theme")
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("env-theme")
|
||||
expect(config.keybinds?.app_exit).toBe("ctrl+q")
|
||||
} finally {
|
||||
if (original === undefined) delete process.env.TUI_THEME_TEST
|
||||
else process.env.TUI_THEME_TEST = original
|
||||
@@ -575,46 +481,8 @@ 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()
|
||||
expect(config.theme).toBe("resolved-theme")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("loads managed tui config and gives it highest precedence", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(
|
||||
path.join(dir, "tui.json"),
|
||||
JSON.stringify({ theme: "project-theme", plugin: ["shared-plugin@1.0.0"] }, null, 2),
|
||||
)
|
||||
await fs.mkdir(managedConfigDir, { recursive: true })
|
||||
await Bun.write(
|
||||
path.join(managedConfigDir, "tui.json"),
|
||||
JSON.stringify({ theme: "managed-theme", plugin: ["shared-plugin@2.0.0"] }, null, 2),
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.theme).toBe("managed-theme")
|
||||
expect(config.plugin).toEqual(["shared-plugin@2.0.0"])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
{
|
||||
spec: "shared-plugin@2.0.0",
|
||||
scope: "global",
|
||||
source: path.join(managedConfigDir, "tui.json"),
|
||||
},
|
||||
])
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.theme).toBe("resolved-theme")
|
||||
})
|
||||
|
||||
test("loads .opencode/tui.json", async () => {
|
||||
@@ -624,33 +492,8 @@ test("loads .opencode/tui.json", async () => {
|
||||
await Bun.write(path.join(dir, ".opencode", "tui.json"), JSON.stringify({ diff_style: "stacked" }, null, 2))
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("gracefully falls back when tui.json has invalid JSON", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
init: async (dir) => {
|
||||
await Bun.write(path.join(dir, "tui.json"), "{ invalid json }")
|
||||
await fs.mkdir(managedConfigDir, { recursive: true })
|
||||
await Bun.write(path.join(managedConfigDir, "tui.json"), JSON.stringify({ theme: "managed-fallback" }, null, 2))
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.theme).toBe("managed-fallback")
|
||||
expect(config.keybinds).toBeDefined()
|
||||
},
|
||||
})
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.diff_style).toBe("stacked")
|
||||
})
|
||||
|
||||
test("supports tuple plugin specs with options in tui.json", async () => {
|
||||
@@ -665,20 +508,15 @@ test("supports tuple plugin specs with options in tui.json", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.plugin).toEqual([["acme-plugin@1.2.3", { enabled: true, label: "demo" }]])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
{
|
||||
spec: ["acme-plugin@1.2.3", { enabled: true, label: "demo" }],
|
||||
scope: "local",
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
])
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.plugin).toEqual([["acme-plugin@1.2.3", { enabled: true, label: "demo" }]])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
{
|
||||
spec: ["acme-plugin@1.2.3", { enabled: true, label: "demo" }],
|
||||
scope: "local",
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
test("deduplicates tuple plugin specs by name with higher precedence winning", async () => {
|
||||
@@ -702,28 +540,23 @@ 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()
|
||||
expect(config.plugin).toEqual([
|
||||
["acme-plugin@2.0.0", { source: "project" }],
|
||||
["second-plugin@3.0.0", { source: "project" }],
|
||||
])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
{
|
||||
spec: ["acme-plugin@2.0.0", { source: "project" }],
|
||||
scope: "local",
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
{
|
||||
spec: ["second-plugin@3.0.0", { source: "project" }],
|
||||
scope: "local",
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
])
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.plugin).toEqual([
|
||||
["acme-plugin@2.0.0", { source: "project" }],
|
||||
["second-plugin@3.0.0", { source: "project" }],
|
||||
])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
{
|
||||
spec: ["acme-plugin@2.0.0", { source: "project" }],
|
||||
scope: "local",
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
})
|
||||
{
|
||||
spec: ["second-plugin@3.0.0", { source: "project" }],
|
||||
scope: "local",
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("tracks global and local plugin metadata in merged tui config", async () => {
|
||||
@@ -744,25 +577,20 @@ 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()
|
||||
expect(config.plugin).toEqual(["global-plugin@1.0.0", "local-plugin@2.0.0"])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
{
|
||||
spec: "global-plugin@1.0.0",
|
||||
scope: "global",
|
||||
source: path.join(Global.Path.config, "tui.json"),
|
||||
},
|
||||
{
|
||||
spec: "local-plugin@2.0.0",
|
||||
scope: "local",
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
])
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.plugin).toEqual(["global-plugin@1.0.0", "local-plugin@2.0.0"])
|
||||
expect(config.plugin_origins).toEqual([
|
||||
{
|
||||
spec: "global-plugin@1.0.0",
|
||||
scope: "global",
|
||||
source: path.join(Global.Path.config, "tui.json"),
|
||||
},
|
||||
})
|
||||
{
|
||||
spec: "local-plugin@2.0.0",
|
||||
scope: "local",
|
||||
source: path.join(tmp.path, "tui.json"),
|
||||
},
|
||||
])
|
||||
})
|
||||
|
||||
test("merges plugin_enabled flags across config layers", async () => {
|
||||
@@ -789,15 +617,10 @@ test("merges plugin_enabled flags across config layers", async () => {
|
||||
},
|
||||
})
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const config = await TuiConfig.get()
|
||||
expect(config.plugin_enabled).toEqual({
|
||||
"internal:sidebar-context": false,
|
||||
"demo.plugin": false,
|
||||
"local.plugin": true,
|
||||
})
|
||||
},
|
||||
const config = await getTuiConfig(tmp.path)
|
||||
expect(config.plugin_enabled).toEqual({
|
||||
"internal:sidebar-context": false,
|
||||
"demo.plugin": false,
|
||||
"local.plugin": true,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -140,7 +140,7 @@ describe("file/index Filesystem patterns", () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(Filesystem.mimeType(filepath)).toContain("application/json")
|
||||
expect(await Filesystem.mimeType(filepath)).toContain("application/json")
|
||||
|
||||
const result = await read("test.json")
|
||||
expect(result.type).toBe("text")
|
||||
@@ -164,7 +164,7 @@ describe("file/index Filesystem patterns", () => {
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
expect(Filesystem.mimeType(filepath)).toContain(mime)
|
||||
expect(await Filesystem.mimeType(filepath)).toContain(mime)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,27 +1,31 @@
|
||||
import { spyOn } from "bun:test"
|
||||
import path from "path"
|
||||
import { TuiConfig } from "../../src/config"
|
||||
import { TuiConfig } from "../../src/cli/cmd/tui/config/tui"
|
||||
|
||||
type PluginSpec = string | [string, Record<string, unknown>]
|
||||
|
||||
export function mockTuiRuntime(dir: string, plugin: PluginSpec[]) {
|
||||
export function mockTuiRuntime(dir: string, plugin: PluginSpec[], opts?: { plugin_enabled?: Record<string, boolean> }) {
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(dir, "plugin-meta.json")
|
||||
const plugin_origins = plugin.map((spec) => ({
|
||||
spec,
|
||||
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 cwd = spyOn(process, "cwd").mockImplementation(() => dir)
|
||||
|
||||
return () => {
|
||||
cwd.mockRestore()
|
||||
get.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
const config: TuiConfig.Info = {
|
||||
plugin,
|
||||
plugin_origins,
|
||||
...(opts?.plugin_enabled && { plugin_enabled: opts.plugin_enabled }),
|
||||
}
|
||||
|
||||
return {
|
||||
config,
|
||||
restore: () => {
|
||||
cwd.mockRestore()
|
||||
wait.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import path from "path"
|
||||
import { Global } from "../../src/global"
|
||||
import { Installation } from "../../src/installation"
|
||||
import { InstallationChannel } from "../../src/installation/version"
|
||||
import { Database } from "../../src/storage"
|
||||
|
||||
describe("Database.Path", () => {
|
||||
test("returns database path for the current channel", () => {
|
||||
const expected = ["latest", "beta"].includes(Installation.CHANNEL)
|
||||
const expected = ["latest", "beta"].includes(InstallationChannel)
|
||||
? path.join(Global.Path.data, "opencode.db")
|
||||
: path.join(Global.Path.data, `opencode-${Installation.CHANNEL.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
||||
: path.join(Global.Path.data, `opencode-${InstallationChannel.replace(/[^a-zA-Z0-9._-]/g, "-")}.db`)
|
||||
expect(Database.getChannelPath()).toBe(expected)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Tool } from "../../src/tool"
|
||||
import { Filesystem } from "../../src/util"
|
||||
import { provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||
import { testEffect } from "../lib/effect"
|
||||
import { Npm } from "@opencode-ai/shared/npm"
|
||||
|
||||
const FIXTURES_DIR = path.join(import.meta.dir, "fixtures")
|
||||
|
||||
|
||||
@@ -347,31 +347,31 @@ describe("filesystem", () => {
|
||||
})
|
||||
|
||||
describe("mimeType()", () => {
|
||||
test("returns correct MIME type for JSON", () => {
|
||||
expect(Filesystem.mimeType("test.json")).toContain("application/json")
|
||||
test("returns correct MIME type for JSON", async () => {
|
||||
expect(await Filesystem.mimeType("test.json")).toContain("application/json")
|
||||
})
|
||||
|
||||
test("returns correct MIME type for JavaScript", () => {
|
||||
expect(Filesystem.mimeType("test.js")).toContain("javascript")
|
||||
test("returns correct MIME type for JavaScript", async () => {
|
||||
expect(await Filesystem.mimeType("test.js")).toContain("javascript")
|
||||
})
|
||||
|
||||
test("returns MIME type for TypeScript (or video/mp2t due to extension conflict)", () => {
|
||||
const mime = Filesystem.mimeType("test.ts")
|
||||
test("returns MIME type for TypeScript (or video/mp2t due to extension conflict)", async () => {
|
||||
const mime = await Filesystem.mimeType("test.ts")
|
||||
// .ts is ambiguous: TypeScript vs MPEG-2 TS video
|
||||
expect(mime === "video/mp2t" || mime === "application/typescript" || mime === "text/typescript").toBe(true)
|
||||
})
|
||||
|
||||
test("returns correct MIME type for images", () => {
|
||||
expect(Filesystem.mimeType("test.png")).toContain("image/png")
|
||||
expect(Filesystem.mimeType("test.jpg")).toContain("image/jpeg")
|
||||
test("returns correct MIME type for images", async () => {
|
||||
expect(await Filesystem.mimeType("test.png")).toContain("image/png")
|
||||
expect(await Filesystem.mimeType("test.jpg")).toContain("image/jpeg")
|
||||
})
|
||||
|
||||
test("returns default for unknown extension", () => {
|
||||
expect(Filesystem.mimeType("test.unknown")).toBe("application/octet-stream")
|
||||
test("returns default for unknown extension", async () => {
|
||||
expect(await Filesystem.mimeType("test.unknown")).toBe("application/octet-stream")
|
||||
})
|
||||
|
||||
test("handles files without extension", () => {
|
||||
expect(Filesystem.mimeType("Makefile")).toBe("application/octet-stream")
|
||||
test("handles files without extension", async () => {
|
||||
expect(await Filesystem.mimeType("Makefile")).toBe("application/octet-stream")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user