Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 47 additions & 71 deletions packages/opencode/test/tool/edit.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { afterAll, afterEach, describe, test, expect } from "bun:test"
import { afterEach, describe, expect } from "bun:test"
import path from "path"
import fs from "fs/promises"
import { Cause, Deferred, Effect, Exit, Layer, ManagedRuntime } from "effect"
import { Cause, Deferred, Effect, Exit, Fiber, Layer } from "effect"
import { EditTool } from "../../src/tool/edit"
import { WithInstance } from "../../src/project/with-instance"
import { disposeAllInstances, TestInstance, tmpdir } from "../fixture/fixture"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { LSP } from "@/lsp/lsp"
import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Format } from "../../src/format"
Expand Down Expand Up @@ -42,20 +41,6 @@ const layer = Layer.mergeAll(

const it = testEffect(layer)

const runtime = ManagedRuntime.make(layer)

afterAll(async () => {
await runtime.dispose()
})

const resolve = () =>
runtime.runPromise(
Effect.gen(function* () {
const info = yield* EditTool
return yield* info.init()
}),
)

const init = Effect.fn("EditToolTest.init")(function* () {
const info = yield* EditTool
return yield* info.init()
Expand Down Expand Up @@ -500,58 +485,49 @@ describe("tool.edit", () => {
})

describe("concurrent editing", () => {
test("preserves concurrent edits to different sections of the same file", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "file.txt")
await fs.writeFile(filepath, "top = 0\nmiddle = keep\nbottom = 0\n", "utf-8")

await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
let asks = 0
const firstAsk = Promise.withResolvers<void>()
const delayedCtx = {
...ctx,
ask: () =>
Effect.gen(function* () {
asks++
if (asks !== 1) return
firstAsk.resolve()
yield* Effect.promise(() => Bun.sleep(50))
}),
}

const promise1 = Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "top = 0",
newString: "top = 1",
},
delayedCtx,
),
)

await firstAsk.promise

const promise2 = Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "bottom = 0",
newString: "bottom = 2",
},
delayedCtx,
),
)

const results = await Promise.allSettled([promise1, promise2])
expect(results[0]?.status).toBe("fulfilled")
expect(results[1]?.status).toBe("fulfilled")
expect(await fs.readFile(filepath, "utf-8")).toBe("top = 1\nmiddle = keep\nbottom = 2\n")
},
})
})
it.instance("preserves concurrent edits to different sections of the same file", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "file.txt")
yield* put(filepath, "top = 0\nmiddle = keep\nbottom = 0\n")

const firstAsk = yield* Deferred.make<void>()
let asks = 0
const delayedCtx = {
...ctx,
ask: () =>
Effect.gen(function* () {
asks++
if (asks !== 1) return
yield* Deferred.succeed(firstAsk, undefined)
yield* Effect.promise(() => Bun.sleep(50))
}),
}

const first = yield* run(
{
filePath: filepath,
oldString: "top = 0",
newString: "top = 1",
},
delayedCtx,
).pipe(Effect.forkScoped)

yield* Deferred.await(firstAsk)
yield* Effect.all([
Fiber.join(first),
run(
{
filePath: filepath,
oldString: "bottom = 0",
newString: "bottom = 2",
},
delayedCtx,
),
])

expect(yield* load(filepath)).toBe("top = 1\nmiddle = keep\nbottom = 2\n")
}),
)
})
})
Loading