test(plugin): migrate loader shared tests to Effect runner (#27129)
This commit is contained in:
@@ -1,9 +1,11 @@
|
|||||||
import { afterAll, afterEach, describe, expect, spyOn, test } from "bun:test"
|
import { afterAll, afterEach, describe, expect, spyOn } from "bun:test"
|
||||||
import { Effect, Layer } from "effect"
|
import { Effect, Layer } from "effect"
|
||||||
import fs from "fs/promises"
|
import fs from "fs/promises"
|
||||||
import path from "path"
|
import path from "path"
|
||||||
import { pathToFileURL } from "url"
|
import { pathToFileURL } from "url"
|
||||||
import { disposeAllInstances, provideInstance, tmpdir } from "../fixture/fixture"
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
||||||
|
import { disposeAllInstances, provideInstance, tmpdirScoped } from "../fixture/fixture"
|
||||||
|
import { testEffect } from "../lib/effect"
|
||||||
import { Filesystem } from "@/util/filesystem"
|
import { Filesystem } from "@/util/filesystem"
|
||||||
|
|
||||||
const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
|
const disableDefault = process.env.OPENCODE_DISABLE_DEFAULT_PLUGINS
|
||||||
@@ -28,11 +30,27 @@ afterEach(async () => {
|
|||||||
await disposeAllInstances()
|
await disposeAllInstances()
|
||||||
})
|
})
|
||||||
|
|
||||||
async function load(dir: string) {
|
const it = testEffect(CrossSpawnSpawner.defaultLayer)
|
||||||
const source = path.join(dir, "opencode.json")
|
|
||||||
const config = (await Bun.file(source).json()) as { plugin?: Array<string | [string, Record<string, unknown>]> }
|
function withTmp<T, A, E, R>(
|
||||||
const plugins = config.plugin ?? []
|
init: (dir: string) => Promise<T>,
|
||||||
|
body: (tmp: { path: string; extra: T }) => Effect.Effect<A, E, R>,
|
||||||
|
) {
|
||||||
return Effect.gen(function* () {
|
return Effect.gen(function* () {
|
||||||
|
const dir = yield* tmpdirScoped()
|
||||||
|
const extra = yield* Effect.promise(() => init(dir))
|
||||||
|
return yield* body({ path: dir, extra })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function load(dir: string) {
|
||||||
|
const source = path.join(dir, "opencode.json")
|
||||||
|
return Effect.gen(function* () {
|
||||||
|
const config = yield* Effect.promise(
|
||||||
|
() => Bun.file(source).json() as Promise<{ plugin?: Array<string | [string, Record<string, unknown>]> }>,
|
||||||
|
)
|
||||||
|
const plugins = config.plugin ?? []
|
||||||
|
return yield* Effect.gen(function* () {
|
||||||
const plugin = yield* Plugin.Service
|
const plugin = yield* Plugin.Service
|
||||||
yield* plugin.list()
|
yield* plugin.list()
|
||||||
}).pipe(
|
}).pipe(
|
||||||
@@ -52,14 +70,14 @@ async function load(dir: string) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
provideInstance(dir),
|
provideInstance(dir),
|
||||||
Effect.runPromise,
|
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
describe("plugin.loader.shared", () => {
|
describe("plugin.loader.shared", () => {
|
||||||
test("loads a file:// plugin function export", async () => {
|
it.live("loads a file:// plugin function export", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const file = path.join(dir, "plugin.ts")
|
const file = path.join(dir, "plugin.ts")
|
||||||
const mark = path.join(dir, "called.txt")
|
const mark = path.join(dir, "called.txt")
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -80,15 +98,17 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* load(tmp.path)
|
||||||
|
expect(yield* Effect.promise(() => fs.readFile(tmp.extra.mark, "utf8"))).toBe("called")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await load(tmp.path)
|
it.live("deduplicates same function exported as default and named", () =>
|
||||||
expect(await fs.readFile(tmp.extra.mark, "utf8")).toBe("called")
|
withTmp(
|
||||||
})
|
async (dir) => {
|
||||||
|
|
||||||
test("deduplicates same function exported as default and named", async () => {
|
|
||||||
await using tmp = await tmpdir({
|
|
||||||
init: async (dir) => {
|
|
||||||
const file = path.join(dir, "plugin.ts")
|
const file = path.join(dir, "plugin.ts")
|
||||||
const mark = path.join(dir, "count.txt")
|
const mark = path.join(dir, "count.txt")
|
||||||
await Bun.write(mark, "")
|
await Bun.write(mark, "")
|
||||||
@@ -113,15 +133,17 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* load(tmp.path)
|
||||||
|
expect(yield* Effect.promise(() => fs.readFile(tmp.extra.mark, "utf8"))).toBe("1")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await load(tmp.path)
|
it.live("uses only default v1 server plugin when present", () =>
|
||||||
expect(await fs.readFile(tmp.extra.mark, "utf8")).toBe("1")
|
withTmp(
|
||||||
})
|
async (dir) => {
|
||||||
|
|
||||||
test("uses only default v1 server plugin when present", async () => {
|
|
||||||
await using tmp = await tmpdir({
|
|
||||||
init: async (dir) => {
|
|
||||||
const file = path.join(dir, "plugin.ts")
|
const file = path.join(dir, "plugin.ts")
|
||||||
const mark = path.join(dir, "count.txt")
|
const mark = path.join(dir, "count.txt")
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -149,15 +171,17 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* load(tmp.path)
|
||||||
|
expect(yield* Effect.promise(() => Bun.file(tmp.extra.mark).text())).toBe("default")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await load(tmp.path)
|
it.live("rejects v1 file server plugin without id", () =>
|
||||||
expect(await Bun.file(tmp.extra.mark).text()).toBe("default")
|
withTmp(
|
||||||
})
|
async (dir) => {
|
||||||
|
|
||||||
test("rejects v1 file server plugin without id", async () => {
|
|
||||||
await using tmp = await tmpdir({
|
|
||||||
init: async (dir) => {
|
|
||||||
const file = path.join(dir, "plugin.ts")
|
const file = path.join(dir, "plugin.ts")
|
||||||
const mark = path.join(dir, "called.txt")
|
const mark = path.join(dir, "called.txt")
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -180,20 +204,24 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
const called = await Bun.file(tmp.extra.mark)
|
const called = yield* Effect.promise(() =>
|
||||||
|
Bun.file(tmp.extra.mark)
|
||||||
.text()
|
.text()
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false),
|
||||||
|
)
|
||||||
|
|
||||||
expect(called).toBe(false)
|
expect(called).toBe(false)
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("rejects v1 plugin that exports server and tui together", async () => {
|
it.live("rejects v1 plugin that exports server and tui together", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const file = path.join(dir, "plugin.ts")
|
const file = path.join(dir, "plugin.ts")
|
||||||
const mark = path.join(dir, "called.txt")
|
const mark = path.join(dir, "called.txt")
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -218,20 +246,24 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
const called = await Bun.file(tmp.extra.mark)
|
const called = yield* Effect.promise(() =>
|
||||||
|
Bun.file(tmp.extra.mark)
|
||||||
.text()
|
.text()
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false),
|
||||||
|
)
|
||||||
|
|
||||||
expect(called).toBe(false)
|
expect(called).toBe(false)
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("resolves npm plugin specs with explicit and default versions", async () => {
|
it.live("resolves npm plugin specs with explicit and default versions", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const acme = path.join(dir, "node_modules", "acme-plugin")
|
const acme = path.join(dir, "node_modules", "acme-plugin")
|
||||||
const scope = path.join(dir, "node_modules", "scope-plugin")
|
const scope = path.join(dir, "node_modules", "scope-plugin")
|
||||||
await fs.mkdir(acme, { recursive: true })
|
await fs.mkdir(acme, { recursive: true })
|
||||||
@@ -254,26 +286,28 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { acme, scope }
|
return { acme, scope }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const add = spyOn(Npm, "add").mockImplementation(async (pkg) => {
|
const add = spyOn(Npm, "add").mockImplementation(async (pkg) => {
|
||||||
if (pkg === "acme-plugin") return { directory: tmp.extra.acme, entrypoint: undefined }
|
if (pkg === "acme-plugin") return { directory: tmp.extra.acme, entrypoint: undefined }
|
||||||
return { directory: tmp.extra.scope, entrypoint: undefined }
|
return { directory: tmp.extra.scope, entrypoint: undefined }
|
||||||
})
|
})
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
|
|
||||||
expect(add.mock.calls).toContainEqual(["acme-plugin@latest"])
|
expect(add.mock.calls).toContainEqual(["acme-plugin@latest"])
|
||||||
expect(add.mock.calls).toContainEqual(["scope-plugin@2.3.4"])
|
expect(add.mock.calls).toContainEqual(["scope-plugin@2.3.4"])
|
||||||
} finally {
|
} finally {
|
||||||
add.mockRestore()
|
add.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("loads npm server plugin from package ./server export", async () => {
|
it.live("loads npm server plugin from package ./server export", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const mod = path.join(dir, "mods", "acme-plugin")
|
const mod = path.join(dir, "mods", "acme-plugin")
|
||||||
const mark = path.join(dir, "server-called.txt")
|
const mark = path.join(dir, "server-called.txt")
|
||||||
await fs.mkdir(mod, { recursive: true })
|
await fs.mkdir(mod, { recursive: true })
|
||||||
@@ -317,21 +351,23 @@ describe("plugin.loader.shared", () => {
|
|||||||
mark,
|
mark,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
expect(await Bun.file(tmp.extra.mark).text()).toBe("called")
|
expect(yield* Effect.promise(() => Bun.file(tmp.extra.mark).text())).toBe("called")
|
||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("loads npm server plugin from package server export without leading dot", async () => {
|
it.live("loads npm server plugin from package server export without leading dot", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const mod = path.join(dir, "mods", "acme-plugin")
|
const mod = path.join(dir, "mods", "acme-plugin")
|
||||||
const dist = path.join(mod, "dist")
|
const dist = path.join(mod, "dist")
|
||||||
const mark = path.join(dir, "server-called.txt")
|
const mark = path.join(dir, "server-called.txt")
|
||||||
@@ -374,21 +410,23 @@ describe("plugin.loader.shared", () => {
|
|||||||
mark,
|
mark,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
expect(await Bun.file(tmp.extra.mark).text()).toBe("called")
|
expect(yield* Effect.promise(() => Bun.file(tmp.extra.mark).text())).toBe("called")
|
||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("loads npm server plugin from package main without leading dot", async () => {
|
it.live("loads npm server plugin from package main without leading dot", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const mod = path.join(dir, "mods", "acme-plugin")
|
const mod = path.join(dir, "mods", "acme-plugin")
|
||||||
const dist = path.join(mod, "dist")
|
const dist = path.join(mod, "dist")
|
||||||
const mark = path.join(dir, "main-called.txt")
|
const mark = path.join(dir, "main-called.txt")
|
||||||
@@ -426,21 +464,23 @@ describe("plugin.loader.shared", () => {
|
|||||||
mark,
|
mark,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
expect(await Bun.file(tmp.extra.mark).text()).toBe("called")
|
expect(yield* Effect.promise(() => Bun.file(tmp.extra.mark).text())).toBe("called")
|
||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("does not use npm package exports dot for server entry", async () => {
|
it.live("does not use npm package exports dot for server entry", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const mod = path.join(dir, "mods", "acme-plugin")
|
const mod = path.join(dir, "mods", "acme-plugin")
|
||||||
const mark = path.join(dir, "dot-server.txt")
|
const mark = path.join(dir, "dot-server.txt")
|
||||||
await fs.mkdir(mod, { recursive: true })
|
await fs.mkdir(mod, { recursive: true })
|
||||||
@@ -471,26 +511,30 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mod, mark }
|
return { mod, mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
const called = await Bun.file(tmp.extra.mark)
|
const called = yield* Effect.promise(() =>
|
||||||
|
Bun.file(tmp.extra.mark)
|
||||||
.text()
|
.text()
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false),
|
||||||
|
)
|
||||||
|
|
||||||
expect(called).toBe(false)
|
expect(called).toBe(false)
|
||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("rejects npm server export that resolves outside plugin directory", async () => {
|
it.live("rejects npm server export that resolves outside plugin directory", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const mod = path.join(dir, "mods", "acme-plugin")
|
const mod = path.join(dir, "mods", "acme-plugin")
|
||||||
const outside = path.join(dir, "outside")
|
const outside = path.join(dir, "outside")
|
||||||
const mark = path.join(dir, "outside-server.txt")
|
const mark = path.join(dir, "outside-server.txt")
|
||||||
@@ -534,25 +578,29 @@ describe("plugin.loader.shared", () => {
|
|||||||
mark,
|
mark,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
const called = await Bun.file(tmp.extra.mark)
|
const called = yield* Effect.promise(() =>
|
||||||
|
Bun.file(tmp.extra.mark)
|
||||||
.text()
|
.text()
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false),
|
||||||
|
)
|
||||||
expect(called).toBe(false)
|
expect(called).toBe(false)
|
||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("skips legacy codex and copilot auth plugin specs", async () => {
|
it.live("skips legacy codex and copilot auth plugin specs", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
path.join(dir, "opencode.json"),
|
path.join(dir, "opencode.json"),
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
@@ -564,12 +612,12 @@ describe("plugin.loader.shared", () => {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
})
|
(_tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: "", entrypoint: undefined })
|
const install = spyOn(Npm, "add").mockResolvedValue({ directory: "", entrypoint: undefined })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await load(tmp.path)
|
yield* load(_tmp.path)
|
||||||
|
|
||||||
const pkgs = install.mock.calls.map((call) => call[0])
|
const pkgs = install.mock.calls.map((call) => call[0])
|
||||||
expect(pkgs).toContain("regular-plugin@1.0.0")
|
expect(pkgs).toContain("regular-plugin@1.0.0")
|
||||||
@@ -578,11 +626,13 @@ describe("plugin.loader.shared", () => {
|
|||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("skips broken plugin when install fails", async () => {
|
it.live("skips broken plugin when install fails", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const ok = path.join(dir, "ok.ts")
|
const ok = path.join(dir, "ok.ts")
|
||||||
const mark = path.join(dir, "ok.txt")
|
const mark = path.join(dir, "ok.txt")
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -604,22 +654,24 @@ describe("plugin.loader.shared", () => {
|
|||||||
)
|
)
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockRejectedValue(new Error("boom"))
|
const install = spyOn(Npm, "add").mockRejectedValue(new Error("boom"))
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
expect(install).toHaveBeenCalledWith("broken-plugin@9.9.9")
|
expect(install).toHaveBeenCalledWith("broken-plugin@9.9.9")
|
||||||
expect(await Bun.file(tmp.extra.mark).text()).toBe("ok")
|
expect(yield* Effect.promise(() => Bun.file(tmp.extra.mark).text())).toBe("ok")
|
||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("continues loading plugins when plugin init throws", async () => {
|
it.live("continues loading plugins when plugin init throws", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const file = pathToFileURL(path.join(dir, "throws.ts")).href
|
const file = pathToFileURL(path.join(dir, "throws.ts")).href
|
||||||
const ok = pathToFileURL(path.join(dir, "ok.ts")).href
|
const ok = pathToFileURL(path.join(dir, "ok.ts")).href
|
||||||
const mark = path.join(dir, "ok.txt")
|
const mark = path.join(dir, "ok.txt")
|
||||||
@@ -653,15 +705,17 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* load(tmp.path)
|
||||||
|
expect(yield* Effect.promise(() => Bun.file(tmp.extra.mark).text())).toBe("ok")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await load(tmp.path)
|
it.live("continues loading plugins when plugin module has invalid export", () =>
|
||||||
expect(await Bun.file(tmp.extra.mark).text()).toBe("ok")
|
withTmp(
|
||||||
})
|
async (dir) => {
|
||||||
|
|
||||||
test("continues loading plugins when plugin module has invalid export", async () => {
|
|
||||||
await using tmp = await tmpdir({
|
|
||||||
init: async (dir) => {
|
|
||||||
const file = pathToFileURL(path.join(dir, "invalid.ts")).href
|
const file = pathToFileURL(path.join(dir, "invalid.ts")).href
|
||||||
const ok = pathToFileURL(path.join(dir, "ok.ts")).href
|
const ok = pathToFileURL(path.join(dir, "ok.ts")).href
|
||||||
const mark = path.join(dir, "ok.txt")
|
const mark = path.join(dir, "ok.txt")
|
||||||
@@ -687,15 +741,17 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* load(tmp.path)
|
||||||
|
expect(yield* Effect.promise(() => Bun.file(tmp.extra.mark).text())).toBe("ok")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await load(tmp.path)
|
it.live("continues loading plugins when plugin import fails", () =>
|
||||||
expect(await Bun.file(tmp.extra.mark).text()).toBe("ok")
|
withTmp(
|
||||||
})
|
async (dir) => {
|
||||||
|
|
||||||
test("continues loading plugins when plugin import fails", async () => {
|
|
||||||
await using tmp = await tmpdir({
|
|
||||||
init: async (dir) => {
|
|
||||||
const missing = pathToFileURL(path.join(dir, "missing-plugin.ts")).href
|
const missing = pathToFileURL(path.join(dir, "missing-plugin.ts")).href
|
||||||
const ok = pathToFileURL(path.join(dir, "ok.ts")).href
|
const ok = pathToFileURL(path.join(dir, "ok.ts")).href
|
||||||
const mark = path.join(dir, "ok.txt")
|
const mark = path.join(dir, "ok.txt")
|
||||||
@@ -716,15 +772,17 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* load(tmp.path)
|
||||||
|
expect(yield* Effect.promise(() => Bun.file(tmp.extra.mark).text())).toBe("ok")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await load(tmp.path)
|
it.live("loads object plugin via plugin.server", () =>
|
||||||
expect(await Bun.file(tmp.extra.mark).text()).toBe("ok")
|
withTmp(
|
||||||
})
|
async (dir) => {
|
||||||
|
|
||||||
test("loads object plugin via plugin.server", async () => {
|
|
||||||
await using tmp = await tmpdir({
|
|
||||||
init: async (dir) => {
|
|
||||||
const file = path.join(dir, "object-plugin.ts")
|
const file = path.join(dir, "object-plugin.ts")
|
||||||
const mark = path.join(dir, "object-called.txt")
|
const mark = path.join(dir, "object-called.txt")
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -749,15 +807,17 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
|
yield* load(tmp.path)
|
||||||
|
expect(yield* Effect.promise(() => fs.readFile(tmp.extra.mark, "utf8"))).toBe("called")
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
await load(tmp.path)
|
it.live("passes tuple plugin options into server plugin", () =>
|
||||||
expect(await fs.readFile(tmp.extra.mark, "utf8")).toBe("called")
|
withTmp(
|
||||||
})
|
async (dir) => {
|
||||||
|
|
||||||
test("passes tuple plugin options into server plugin", async () => {
|
|
||||||
await using tmp = await tmpdir({
|
|
||||||
init: async (dir) => {
|
|
||||||
const file = path.join(dir, "options-plugin.ts")
|
const file = path.join(dir, "options-plugin.ts")
|
||||||
const mark = path.join(dir, "options.json")
|
const mark = path.join(dir, "options.json")
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -782,18 +842,20 @@ describe("plugin.loader.shared", () => {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
expect(await Filesystem.readJson<{ source: string; enabled: boolean }>(tmp.extra.mark)).toEqual({
|
expect(yield* Effect.promise(() => Filesystem.readJson<{ source: string; enabled: boolean }>(tmp.extra.mark))).toEqual({
|
||||||
source: "tuple",
|
source: "tuple",
|
||||||
enabled: true,
|
enabled: true,
|
||||||
})
|
})
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("initializes server plugins in config order", async () => {
|
it.live("initializes server plugins in config order", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const a = path.join(dir, "a-plugin.ts")
|
const a = path.join(dir, "a-plugin.ts")
|
||||||
const b = path.join(dir, "b-plugin.ts")
|
const b = path.join(dir, "b-plugin.ts")
|
||||||
const marker = path.join(dir, "server-order.txt")
|
const marker = path.join(dir, "server-order.txt")
|
||||||
@@ -833,16 +895,18 @@ export default {
|
|||||||
|
|
||||||
return { marker }
|
return { marker }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
const lines = (await fs.readFile(tmp.extra.marker, "utf8")).trim().split("\n")
|
const lines = (yield* Effect.promise(() => fs.readFile(tmp.extra.marker, "utf8"))).trim().split("\n")
|
||||||
expect(lines).toEqual(["a-start", "a-end", "b"])
|
expect(lines).toEqual(["a-start", "a-end", "b"])
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("skips external plugins in pure mode", async () => {
|
it.live("skips external plugins in pure mode", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const file = path.join(dir, "plugin.ts")
|
const file = path.join(dir, "plugin.ts")
|
||||||
const mark = path.join(dir, "called.txt")
|
const mark = path.join(dir, "called.txt")
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -866,17 +930,19 @@ export default {
|
|||||||
|
|
||||||
return { mark }
|
return { mark }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const pure = process.env.OPENCODE_PURE
|
const pure = process.env.OPENCODE_PURE
|
||||||
process.env.OPENCODE_PURE = "1"
|
process.env.OPENCODE_PURE = "1"
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await load(tmp.path)
|
yield* load(tmp.path)
|
||||||
const called = await fs
|
const called = yield* Effect.promise(() =>
|
||||||
|
fs
|
||||||
.readFile(tmp.extra.mark, "utf8")
|
.readFile(tmp.extra.mark, "utf8")
|
||||||
.then(() => true)
|
.then(() => true)
|
||||||
.catch(() => false)
|
.catch(() => false),
|
||||||
|
)
|
||||||
expect(called).toBe(false)
|
expect(called).toBe(false)
|
||||||
} finally {
|
} finally {
|
||||||
if (pure === undefined) {
|
if (pure === undefined) {
|
||||||
@@ -885,11 +951,13 @@ export default {
|
|||||||
process.env.OPENCODE_PURE = pure
|
process.env.OPENCODE_PURE = pure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("reads oc-themes from package manifest", async () => {
|
it.live("reads oc-themes from package manifest", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const mod = path.join(dir, "mod")
|
const mod = path.join(dir, "mod")
|
||||||
await fs.mkdir(path.join(mod, "themes"), { recursive: true })
|
await fs.mkdir(path.join(mod, "themes"), { recursive: true })
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -907,10 +975,10 @@ export default {
|
|||||||
|
|
||||||
return { mod }
|
return { mod }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const file = path.join(tmp.extra.mod, "package.json")
|
const file = path.join(tmp.extra.mod, "package.json")
|
||||||
const json = await Filesystem.readJson<Record<string, unknown>>(file)
|
const json = yield* Effect.promise(() => Filesystem.readJson<Record<string, unknown>>(file))
|
||||||
const list = readPackageThemes("acme-plugin", {
|
const list = readPackageThemes("acme-plugin", {
|
||||||
dir: tmp.extra.mod,
|
dir: tmp.extra.mod,
|
||||||
pkg: file,
|
pkg: file,
|
||||||
@@ -921,11 +989,13 @@ export default {
|
|||||||
Filesystem.resolve(path.join(tmp.extra.mod, "themes", "one.json")),
|
Filesystem.resolve(path.join(tmp.extra.mod, "themes", "one.json")),
|
||||||
Filesystem.resolve(path.join(tmp.extra.mod, "themes", "two.json")),
|
Filesystem.resolve(path.join(tmp.extra.mod, "themes", "two.json")),
|
||||||
])
|
])
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("handles no-entrypoint tui packages via missing callback", async () => {
|
it.live("handles no-entrypoint tui packages via missing callback", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const mod = path.join(dir, "mods", "acme-plugin")
|
const mod = path.join(dir, "mods", "acme-plugin")
|
||||||
await fs.mkdir(path.join(mod, "themes"), { recursive: true })
|
await fs.mkdir(path.join(mod, "themes"), { recursive: true })
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -943,13 +1013,14 @@ export default {
|
|||||||
await Bun.write(path.join(mod, "themes", "night.json"), "{}\n")
|
await Bun.write(path.join(mod, "themes", "night.json"), "{}\n")
|
||||||
return { mod }
|
return { mod }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
||||||
const missing: string[] = []
|
const missing: string[] = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loaded = await PluginLoader.loadExternal({
|
const loaded = yield* Effect.promise(() =>
|
||||||
|
PluginLoader.loadExternal({
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
spec: "acme-plugin@1.0.0",
|
spec: "acme-plugin@1.0.0",
|
||||||
@@ -973,7 +1044,8 @@ export default {
|
|||||||
missing.push(message)
|
missing.push(message)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
expect(loaded).toEqual([
|
expect(loaded).toEqual([
|
||||||
{
|
{
|
||||||
@@ -986,11 +1058,13 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("passes package metadata for entrypoint tui plugins", async () => {
|
it.live("passes package metadata for entrypoint tui plugins", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const mod = path.join(dir, "mods", "acme-plugin")
|
const mod = path.join(dir, "mods", "acme-plugin")
|
||||||
await fs.mkdir(path.join(mod, "themes"), { recursive: true })
|
await fs.mkdir(path.join(mod, "themes"), { recursive: true })
|
||||||
await Bun.write(
|
await Bun.write(
|
||||||
@@ -1012,12 +1086,13 @@ export default {
|
|||||||
await Bun.write(path.join(mod, "themes", "night.json"), "{}\n")
|
await Bun.write(path.join(mod, "themes", "night.json"), "{}\n")
|
||||||
return { mod }
|
return { mod }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
const install = spyOn(Npm, "add").mockResolvedValue({ directory: tmp.extra.mod, entrypoint: undefined })
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loaded = await PluginLoader.loadExternal({
|
const loaded = yield* Effect.promise(() =>
|
||||||
|
PluginLoader.loadExternal({
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
spec: "acme-plugin@1.0.0",
|
spec: "acme-plugin@1.0.0",
|
||||||
@@ -1033,7 +1108,8 @@ export default {
|
|||||||
themes: readPackageThemes(item.spec, item.pkg),
|
themes: readPackageThemes(item.spec, item.pkg),
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
expect(loaded).toEqual([
|
expect(loaded).toEqual([
|
||||||
{
|
{
|
||||||
@@ -1044,20 +1120,22 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("rejects oc-themes path traversal", async () => {
|
it.live("rejects oc-themes path traversal", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const mod = path.join(dir, "mod")
|
const mod = path.join(dir, "mod")
|
||||||
await fs.mkdir(mod, { recursive: true })
|
await fs.mkdir(mod, { recursive: true })
|
||||||
const file = path.join(mod, "package.json")
|
const file = path.join(mod, "package.json")
|
||||||
await Bun.write(file, JSON.stringify({ name: "acme", "oc-themes": ["../escape.json"] }, null, 2))
|
await Bun.write(file, JSON.stringify({ name: "acme", "oc-themes": ["../escape.json"] }, null, 2))
|
||||||
return { mod, file }
|
return { mod, file }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const json = await Filesystem.readJson<Record<string, unknown>>(tmp.extra.file)
|
const json = yield* Effect.promise(() => Filesystem.readJson<Record<string, unknown>>(tmp.extra.file))
|
||||||
expect(() =>
|
expect(() =>
|
||||||
readPackageThemes("acme", {
|
readPackageThemes("acme", {
|
||||||
dir: tmp.extra.mod,
|
dir: tmp.extra.mod,
|
||||||
@@ -1065,11 +1143,13 @@ export default {
|
|||||||
json,
|
json,
|
||||||
}),
|
}),
|
||||||
).toThrow("outside plugin directory")
|
).toThrow("outside plugin directory")
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("retries failed file plugins once after wait and keeps order", async () => {
|
it.live("retries failed file plugins once after wait and keeps order", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const a = path.join(dir, "a")
|
const a = path.join(dir, "a")
|
||||||
const b = path.join(dir, "b")
|
const b = path.join(dir, "b")
|
||||||
const aSpec = pathToFileURL(a).href
|
const aSpec = pathToFileURL(a).href
|
||||||
@@ -1078,12 +1158,13 @@ export default {
|
|||||||
await fs.mkdir(b, { recursive: true })
|
await fs.mkdir(b, { recursive: true })
|
||||||
return { a, b, aSpec, bSpec }
|
return { a, b, aSpec, bSpec }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
let wait = 0
|
let wait = 0
|
||||||
const calls: Array<[string, boolean]> = []
|
const calls: Array<[string, boolean]> = []
|
||||||
|
|
||||||
const loaded = await PluginLoader.loadExternal({
|
const loaded = yield* Effect.promise(() =>
|
||||||
|
PluginLoader.loadExternal({
|
||||||
items: [tmp.extra.aSpec, tmp.extra.bSpec].map((spec) => ({
|
items: [tmp.extra.aSpec, tmp.extra.bSpec].map((spec) => ({
|
||||||
spec,
|
spec,
|
||||||
scope: "local" as const,
|
scope: "local" as const,
|
||||||
@@ -1100,7 +1181,8 @@ export default {
|
|||||||
calls.push([candidate.plan.spec, retry])
|
calls.push([candidate.plan.spec, retry])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
expect(wait).toBe(1)
|
expect(wait).toBe(1)
|
||||||
expect(calls).toEqual([
|
expect(calls).toEqual([
|
||||||
@@ -1110,22 +1192,25 @@ export default {
|
|||||||
[tmp.extra.bSpec, true],
|
[tmp.extra.bSpec, true],
|
||||||
])
|
])
|
||||||
expect(loaded.map((item) => item.spec)).toEqual([tmp.extra.aSpec, tmp.extra.bSpec])
|
expect(loaded.map((item) => item.spec)).toEqual([tmp.extra.aSpec, tmp.extra.bSpec])
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("retries file plugins when finish returns undefined", async () => {
|
it.live("retries file plugins when finish returns undefined", () =>
|
||||||
await using tmp = await tmpdir({
|
withTmp(
|
||||||
init: async (dir) => {
|
async (dir) => {
|
||||||
const file = path.join(dir, "plugin.ts")
|
const file = path.join(dir, "plugin.ts")
|
||||||
const spec = pathToFileURL(file).href
|
const spec = pathToFileURL(file).href
|
||||||
await Bun.write(file, "export default {}\n")
|
await Bun.write(file, "export default {}\n")
|
||||||
return { spec }
|
return { spec }
|
||||||
},
|
},
|
||||||
})
|
(tmp) =>
|
||||||
|
Effect.gen(function* () {
|
||||||
let wait = 0
|
let wait = 0
|
||||||
let count = 0
|
let count = 0
|
||||||
|
|
||||||
const loaded = await PluginLoader.loadExternal({
|
const loaded = yield* Effect.promise(() =>
|
||||||
|
PluginLoader.loadExternal({
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
spec: tmp.extra.spec,
|
spec: tmp.extra.spec,
|
||||||
@@ -1145,20 +1230,25 @@ export default {
|
|||||||
spec: load.spec,
|
spec: load.spec,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
expect(wait).toBe(1)
|
expect(wait).toBe(1)
|
||||||
expect(count).toBe(2)
|
expect(count).toBe(2)
|
||||||
expect(loaded).toEqual([{ retry: true, spec: tmp.extra.spec }])
|
expect(loaded).toEqual([{ retry: true, spec: tmp.extra.spec }])
|
||||||
})
|
}),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
test("does not wait or retry npm plugin failures", async () => {
|
it.live("does not wait or retry npm plugin failures", () =>
|
||||||
|
Effect.gen(function* () {
|
||||||
const install = spyOn(Npm, "add").mockRejectedValue(new Error("boom"))
|
const install = spyOn(Npm, "add").mockRejectedValue(new Error("boom"))
|
||||||
let wait = 0
|
let wait = 0
|
||||||
const errors: Array<[string, boolean]> = []
|
const errors: Array<[string, boolean]> = []
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loaded = await PluginLoader.loadExternal({
|
const loaded = yield* Effect.promise(() =>
|
||||||
|
PluginLoader.loadExternal({
|
||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
spec: "acme-plugin@1.0.0",
|
spec: "acme-plugin@1.0.0",
|
||||||
@@ -1175,7 +1265,8 @@ export default {
|
|||||||
errors.push([stage, retry])
|
errors.push([stage, retry])
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
expect(loaded).toEqual([])
|
expect(loaded).toEqual([])
|
||||||
expect(wait).toBe(0)
|
expect(wait).toBe(0)
|
||||||
@@ -1183,5 +1274,6 @@ export default {
|
|||||||
} finally {
|
} finally {
|
||||||
install.mockRestore()
|
install.mockRestore()
|
||||||
}
|
}
|
||||||
})
|
}),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user