refactor: finish small effect service adoption cleanups (#22094)
This commit is contained in:
@@ -6,7 +6,6 @@ import { tmpdir } from "../../fixture/fixture"
|
||||
import { createTuiPluginApi } from "../../fixture/tui-plugin"
|
||||
import { Global } from "../../../src/global"
|
||||
import { TuiConfig } from "../../../src/config/tui"
|
||||
import { Config } from "../../../src/config/config"
|
||||
import { Filesystem } from "../../../src/util/filesystem"
|
||||
|
||||
const { allThemes, addTheme } = await import("../../../src/cli/cmd/tui/context/theme")
|
||||
@@ -325,7 +324,6 @@ export default {
|
||||
})
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const install = spyOn(Config, "installDependencies").mockResolvedValue()
|
||||
|
||||
try {
|
||||
expect(addTheme(tmp.extra.preloadedThemeName, { theme: { primary: "#303030" } })).toBe(true)
|
||||
@@ -407,7 +405,6 @@ export default {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
wait.mockRestore()
|
||||
install.mockRestore()
|
||||
if (backup === undefined) {
|
||||
await fs.rm(globalConfigPath, { force: true })
|
||||
} else {
|
||||
@@ -701,7 +698,6 @@ test("updates installed theme when plugin metadata changes", async () => {
|
||||
process.env.OPENCODE_PLUGIN_META_FILE = path.join(tmp.path, "plugin-meta.json")
|
||||
const cwd = spyOn(process, "cwd").mockImplementation(() => tmp.path)
|
||||
const wait = spyOn(TuiConfig, "waitForDependencies").mockResolvedValue()
|
||||
const install = spyOn(Config, "installDependencies").mockResolvedValue()
|
||||
|
||||
const api = () =>
|
||||
createTuiPluginApi({
|
||||
@@ -746,7 +742,6 @@ test("updates installed theme when plugin metadata changes", async () => {
|
||||
await TuiPluginRuntime.dispose()
|
||||
cwd.mockRestore()
|
||||
wait.mockRestore()
|
||||
install.mockRestore()
|
||||
delete process.env.OPENCODE_PLUGIN_META_FILE
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { test, expect, describe, mock, afterEach, beforeEach, spyOn } from "bun:test"
|
||||
import { Effect, Layer, Option } from "effect"
|
||||
import { Deferred, Effect, Fiber, Layer, Option } from "effect"
|
||||
import { NodeFileSystem, NodePath } from "@effect/platform-node"
|
||||
import { Config } from "../../src/config/config"
|
||||
import { Instance } from "../../src/project/instance"
|
||||
@@ -7,8 +7,9 @@ import { Auth } from "../../src/auth"
|
||||
import { AccessToken, Account, AccountID, OrgID } from "../../src/account"
|
||||
import { AppFileSystem } from "../../src/filesystem"
|
||||
import { provideTmpdirInstance } from "../fixture/fixture"
|
||||
import { tmpdir } from "../fixture/fixture"
|
||||
import { tmpdir, tmpdirScoped } from "../fixture/fixture"
|
||||
import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner"
|
||||
import { testEffect } from "../lib/effect"
|
||||
|
||||
/** Infra layer that provides FileSystem, Path, ChildProcessSpawner for test fixtures */
|
||||
const infra = CrossSpawnSpawner.defaultLayer.pipe(
|
||||
@@ -32,6 +33,18 @@ const emptyAuth = Layer.mock(Auth.Service)({
|
||||
all: () => Effect.succeed({}),
|
||||
})
|
||||
|
||||
const it = testEffect(
|
||||
Config.layer.pipe(
|
||||
Layer.provide(AppFileSystem.defaultLayer),
|
||||
Layer.provide(emptyAuth),
|
||||
Layer.provide(emptyAccount),
|
||||
Layer.provideMerge(infra),
|
||||
),
|
||||
)
|
||||
|
||||
const installDeps = (dir: string, input?: Config.InstallInput) =>
|
||||
Config.Service.use((svc) => svc.installDependencies(dir, input))
|
||||
|
||||
// Get managed config directory from environment (set in preload.ts)
|
||||
const managedConfigDir = process.env.OPENCODE_TEST_MANAGED_CONFIG_DIR!
|
||||
|
||||
@@ -817,128 +830,134 @@ test("installs dependencies in writable OPENCODE_CONFIG_DIR", async () => {
|
||||
}
|
||||
})
|
||||
|
||||
test("dedupes concurrent config dependency installs for the same dir", async () => {
|
||||
await using tmp = await tmpdir()
|
||||
const dir = path.join(tmp.path, "a")
|
||||
await fs.mkdir(dir, { recursive: true })
|
||||
it.live("dedupes concurrent config dependency installs for the same dir", () =>
|
||||
Effect.gen(function* () {
|
||||
const tmp = yield* tmpdirScoped()
|
||||
const dir = path.join(tmp, "a")
|
||||
yield* Effect.promise(() => fs.mkdir(dir, { recursive: true }))
|
||||
|
||||
const ticks: number[] = []
|
||||
let calls = 0
|
||||
let start = () => {}
|
||||
let done = () => {}
|
||||
let blocked = () => {}
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
start = resolve
|
||||
})
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
done = resolve
|
||||
})
|
||||
const waiting = new Promise<void>((resolve) => {
|
||||
blocked = resolve
|
||||
})
|
||||
const online = spyOn(Network, "online").mockReturnValue(false)
|
||||
const targetDir = dir
|
||||
const run = spyOn(Npm, "install").mockImplementation(async (d: string) => {
|
||||
const hit = path.normalize(d) === path.normalize(targetDir)
|
||||
if (hit) {
|
||||
let calls = 0
|
||||
const online = spyOn(Network, "online").mockReturnValue(false)
|
||||
const ready = Deferred.makeUnsafe<void>()
|
||||
const blocked = Deferred.makeUnsafe<void>()
|
||||
const hold = Deferred.makeUnsafe<void>()
|
||||
const target = path.normalize(dir)
|
||||
const run = spyOn(Npm, "install").mockImplementation(async (d: string) => {
|
||||
if (path.normalize(d) !== target) return
|
||||
calls += 1
|
||||
start()
|
||||
await gate
|
||||
}
|
||||
const mod = path.join(d, "node_modules", "@opencode-ai", "plugin")
|
||||
await fs.mkdir(mod, { recursive: true })
|
||||
await Filesystem.write(
|
||||
path.join(mod, "package.json"),
|
||||
JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
|
||||
)
|
||||
if (hit) {
|
||||
start()
|
||||
await gate
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const first = Config.installDependencies(dir)
|
||||
await ready
|
||||
const second = Config.installDependencies(dir, {
|
||||
waitTick: (tick) => {
|
||||
ticks.push(tick.attempt)
|
||||
blocked()
|
||||
blocked = () => {}
|
||||
},
|
||||
Deferred.doneUnsafe(ready, Effect.void)
|
||||
await Effect.runPromise(Deferred.await(hold))
|
||||
const mod = path.join(d, "node_modules", "@opencode-ai", "plugin")
|
||||
await fs.mkdir(mod, { recursive: true })
|
||||
await Filesystem.write(
|
||||
path.join(mod, "package.json"),
|
||||
JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
|
||||
)
|
||||
})
|
||||
await waiting
|
||||
done()
|
||||
await Promise.all([first, second])
|
||||
} finally {
|
||||
online.mockRestore()
|
||||
run.mockRestore()
|
||||
}
|
||||
|
||||
expect(calls).toBe(2)
|
||||
expect(ticks.length).toBeGreaterThan(0)
|
||||
expect(await Filesystem.exists(path.join(dir, "package.json"))).toBe(true)
|
||||
})
|
||||
|
||||
test("serializes config dependency installs across dirs", async () => {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
await using tmp = await tmpdir()
|
||||
const a = path.join(tmp.path, "a")
|
||||
const b = path.join(tmp.path, "b")
|
||||
await fs.mkdir(a, { recursive: true })
|
||||
await fs.mkdir(b, { recursive: true })
|
||||
|
||||
let calls = 0
|
||||
let open = 0
|
||||
let peak = 0
|
||||
let start = () => {}
|
||||
let done = () => {}
|
||||
const ready = new Promise<void>((resolve) => {
|
||||
start = resolve
|
||||
})
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
done = resolve
|
||||
})
|
||||
|
||||
const online = spyOn(Network, "online").mockReturnValue(false)
|
||||
const run = spyOn(Npm, "install").mockImplementation(async (dir: string) => {
|
||||
const cwd = path.normalize(dir)
|
||||
const hit = cwd === path.normalize(a) || cwd === path.normalize(b)
|
||||
if (hit) {
|
||||
calls += 1
|
||||
open += 1
|
||||
peak = Math.max(peak, open)
|
||||
if (calls === 1) {
|
||||
start()
|
||||
await gate
|
||||
}
|
||||
}
|
||||
const mod = path.join(cwd, "node_modules", "@opencode-ai", "plugin")
|
||||
await fs.mkdir(mod, { recursive: true })
|
||||
await Filesystem.write(
|
||||
path.join(mod, "package.json"),
|
||||
JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
online.mockRestore()
|
||||
run.mockRestore()
|
||||
}),
|
||||
)
|
||||
if (hit) {
|
||||
open -= 1
|
||||
}
|
||||
})
|
||||
|
||||
try {
|
||||
const first = Config.installDependencies(a)
|
||||
await ready
|
||||
const second = Config.installDependencies(b)
|
||||
done()
|
||||
await Promise.all([first, second])
|
||||
} finally {
|
||||
online.mockRestore()
|
||||
run.mockRestore()
|
||||
}
|
||||
const first = yield* installDeps(dir).pipe(Effect.forkScoped)
|
||||
yield* Deferred.await(ready)
|
||||
|
||||
expect(calls).toBe(2)
|
||||
expect(peak).toBe(1)
|
||||
})
|
||||
let done = false
|
||||
const second = yield* installDeps(dir, {
|
||||
waitTick: () => {
|
||||
Deferred.doneUnsafe(blocked, Effect.void)
|
||||
},
|
||||
}).pipe(
|
||||
Effect.tap(() =>
|
||||
Effect.sync(() => {
|
||||
done = true
|
||||
}),
|
||||
),
|
||||
Effect.forkScoped,
|
||||
)
|
||||
|
||||
yield* Deferred.await(blocked)
|
||||
expect(done).toBe(false)
|
||||
|
||||
yield* Deferred.succeed(hold, void 0)
|
||||
yield* Fiber.join(first)
|
||||
yield* Fiber.join(second)
|
||||
|
||||
expect(calls).toBe(1)
|
||||
expect(yield* Effect.promise(() => Filesystem.exists(path.join(dir, "package.json")))).toBe(true)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("serializes config dependency installs across dirs", () =>
|
||||
Effect.gen(function* () {
|
||||
if (process.platform !== "win32") return
|
||||
|
||||
const tmp = yield* tmpdirScoped()
|
||||
const a = path.join(tmp, "a")
|
||||
const b = path.join(tmp, "b")
|
||||
yield* Effect.promise(() => fs.mkdir(a, { recursive: true }))
|
||||
yield* Effect.promise(() => fs.mkdir(b, { recursive: true }))
|
||||
|
||||
let calls = 0
|
||||
let open = 0
|
||||
let peak = 0
|
||||
const ready = Deferred.makeUnsafe<void>()
|
||||
const blocked = Deferred.makeUnsafe<void>()
|
||||
const hold = Deferred.makeUnsafe<void>()
|
||||
|
||||
const online = spyOn(Network, "online").mockReturnValue(false)
|
||||
const run = spyOn(Npm, "install").mockImplementation(async (dir: string) => {
|
||||
const cwd = path.normalize(dir)
|
||||
const hit = cwd === path.normalize(a) || cwd === path.normalize(b)
|
||||
if (hit) {
|
||||
calls += 1
|
||||
open += 1
|
||||
peak = Math.max(peak, open)
|
||||
if (calls === 1) {
|
||||
Deferred.doneUnsafe(ready, Effect.void)
|
||||
await Effect.runPromise(Deferred.await(hold))
|
||||
}
|
||||
}
|
||||
const mod = path.join(cwd, "node_modules", "@opencode-ai", "plugin")
|
||||
await fs.mkdir(mod, { recursive: true })
|
||||
await Filesystem.write(
|
||||
path.join(mod, "package.json"),
|
||||
JSON.stringify({ name: "@opencode-ai/plugin", version: "1.0.0" }),
|
||||
)
|
||||
if (hit) {
|
||||
open -= 1
|
||||
}
|
||||
})
|
||||
|
||||
yield* Effect.addFinalizer(() =>
|
||||
Effect.sync(() => {
|
||||
online.mockRestore()
|
||||
run.mockRestore()
|
||||
}),
|
||||
)
|
||||
|
||||
const first = yield* installDeps(a).pipe(Effect.forkScoped)
|
||||
yield* Deferred.await(ready)
|
||||
|
||||
const second = yield* installDeps(b, {
|
||||
waitTick: () => {
|
||||
Deferred.doneUnsafe(blocked, Effect.void)
|
||||
},
|
||||
}).pipe(Effect.forkScoped)
|
||||
yield* Deferred.await(blocked)
|
||||
expect(peak).toBe(1)
|
||||
|
||||
yield* Deferred.succeed(hold, void 0)
|
||||
yield* Fiber.join(first)
|
||||
yield* Fiber.join(second)
|
||||
|
||||
expect(calls).toBe(2)
|
||||
expect(peak).toBe(1)
|
||||
}),
|
||||
)
|
||||
|
||||
test("resolves scoped npm plugins in config", async () => {
|
||||
await using tmp = await tmpdir({
|
||||
|
||||
@@ -1842,6 +1842,11 @@ describe("ProviderTransform.message - cache control on gateway", () => {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
alibaba: {
|
||||
cacheControl: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1894,6 +1899,11 @@ describe("ProviderTransform.message - cache control on gateway", () => {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
alibaba: {
|
||||
cacheControl: {
|
||||
type: "ephemeral",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -65,6 +65,18 @@ const readFileTime = (sessionID: SessionID, filepath: string) =>
|
||||
const subscribeBus = <D extends BusEvent.Definition>(def: D, callback: () => unknown) =>
|
||||
runtime.runPromise(Bus.Service.use((bus) => bus.subscribeCallback(def, callback)))
|
||||
|
||||
async function onceBus<D extends BusEvent.Definition>(def: D) {
|
||||
const result = Promise.withResolvers<void>()
|
||||
const unsub = await subscribeBus(def, () => {
|
||||
unsub()
|
||||
result.resolve()
|
||||
})
|
||||
return {
|
||||
wait: result.promise,
|
||||
unsub,
|
||||
}
|
||||
}
|
||||
|
||||
describe("tool.edit", () => {
|
||||
describe("creating new files", () => {
|
||||
test("creates new file when oldString is empty", async () => {
|
||||
@@ -128,23 +140,25 @@ describe("tool.edit", () => {
|
||||
fn: async () => {
|
||||
const { FileWatcher } = await import("../../src/file/watcher")
|
||||
|
||||
const events: string[] = []
|
||||
const unsubUpdated = await subscribeBus(FileWatcher.Event.Updated, () => events.push("updated"))
|
||||
const updated = await onceBus(FileWatcher.Event.Updated)
|
||||
|
||||
const edit = await resolve()
|
||||
await Effect.runPromise(
|
||||
edit.execute(
|
||||
{
|
||||
filePath: filepath,
|
||||
oldString: "",
|
||||
newString: "content",
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
)
|
||||
try {
|
||||
const edit = await resolve()
|
||||
await Effect.runPromise(
|
||||
edit.execute(
|
||||
{
|
||||
filePath: filepath,
|
||||
oldString: "",
|
||||
newString: "content",
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
)
|
||||
|
||||
expect(events).toContain("updated")
|
||||
unsubUpdated()
|
||||
await updated.wait
|
||||
} finally {
|
||||
updated.unsub()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
@@ -359,23 +373,25 @@ describe("tool.edit", () => {
|
||||
|
||||
const { FileWatcher } = await import("../../src/file/watcher")
|
||||
|
||||
const events: string[] = []
|
||||
const unsubUpdated = await subscribeBus(FileWatcher.Event.Updated, () => events.push("updated"))
|
||||
const updated = await onceBus(FileWatcher.Event.Updated)
|
||||
|
||||
const edit = await resolve()
|
||||
await Effect.runPromise(
|
||||
edit.execute(
|
||||
{
|
||||
filePath: filepath,
|
||||
oldString: "original",
|
||||
newString: "modified",
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
)
|
||||
try {
|
||||
const edit = await resolve()
|
||||
await Effect.runPromise(
|
||||
edit.execute(
|
||||
{
|
||||
filePath: filepath,
|
||||
oldString: "original",
|
||||
newString: "modified",
|
||||
},
|
||||
ctx,
|
||||
),
|
||||
)
|
||||
|
||||
expect(events).toContain("updated")
|
||||
unsubUpdated()
|
||||
await updated.wait
|
||||
} finally {
|
||||
updated.unsub()
|
||||
}
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user