test(tool): migrate edit tests to Effect runner (#26977)

This commit is contained in:
Kit Langton
2026-05-11 21:42:17 -04:00
committed by GitHub
parent c43d606f8e
commit ce72020750
+273 -471
View File
@@ -1,19 +1,20 @@
import { afterAll, afterEach, describe, test, expect } from "bun:test" import { afterAll, afterEach, describe, test, expect } from "bun:test"
import path from "path" import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { Effect, Layer, ManagedRuntime } from "effect" import { Cause, Deferred, Effect, Exit, Layer, ManagedRuntime } from "effect"
import { EditTool } from "../../src/tool/edit" import { EditTool } from "../../src/tool/edit"
import { Instance } from "../../src/project/instance"
import { WithInstance } from "../../src/project/with-instance" import { WithInstance } from "../../src/project/with-instance"
import { disposeAllInstances, tmpdir } from "../fixture/fixture" import { disposeAllInstances, TestInstance, tmpdir } from "../fixture/fixture"
import { LSP } from "@/lsp/lsp" import { LSP } from "@/lsp/lsp"
import { AppFileSystem } from "@opencode-ai/core/filesystem" import { AppFileSystem } from "@opencode-ai/core/filesystem"
import { Format } from "../../src/format" import { Format } from "../../src/format"
import { Agent } from "../../src/agent/agent" import { Agent } from "../../src/agent/agent"
import { Bus } from "../../src/bus" import { Bus } from "../../src/bus"
import { BusEvent } from "../../src/bus/bus-event"
import { Truncate } from "@/tool/truncate" import { Truncate } from "@/tool/truncate"
import { SessionID, MessageID } from "../../src/session/schema" import { SessionID, MessageID } from "../../src/session/schema"
import * as Tool from "../../src/tool/tool"
import { testEffect } from "../lib/effect"
import { FileWatcher } from "../../src/file/watcher"
const ctx = { const ctx = {
sessionID: SessionID.make("ses_test-edit-session"), sessionID: SessionID.make("ses_test-edit-session"),
@@ -30,17 +31,19 @@ afterEach(async () => {
await disposeAllInstances() await disposeAllInstances()
}) })
const runtime = ManagedRuntime.make( const layer = Layer.mergeAll(
Layer.mergeAll(
LSP.defaultLayer, LSP.defaultLayer,
AppFileSystem.defaultLayer, AppFileSystem.defaultLayer,
Format.defaultLayer, Format.defaultLayer,
Bus.layer, Bus.layer,
Truncate.defaultLayer, Truncate.defaultLayer,
Agent.defaultLayer, Agent.defaultLayer,
),
) )
const it = testEffect(layer)
const runtime = ManagedRuntime.make(layer)
afterAll(async () => { afterAll(async () => {
await runtime.dispose() await runtime.dispose()
}) })
@@ -53,464 +56,258 @@ const resolve = () =>
}), }),
) )
const subscribeBus = <D extends BusEvent.Definition>(def: D, callback: () => unknown) => const init = Effect.fn("EditToolTest.init")(function* () {
runtime.runPromise(Bus.Service.use((bus) => bus.subscribeCallback(def, callback))) const info = yield* EditTool
return yield* info.init()
})
async function onceBus<D extends BusEvent.Definition>(def: D) { const run = Effect.fn("EditToolTest.run")(function* (
const result = Promise.withResolvers<void>() args: Tool.InferParameters<typeof EditTool>,
const unsub = await subscribeBus(def, () => { next: Tool.Context = ctx,
unsub() ) {
result.resolve() const tool = yield* init()
}) return yield* tool.execute(args, next)
return { })
wait: result.promise,
unsub, const fail = Effect.fn("EditToolTest.fail")(function* (args: Tool.InferParameters<typeof EditTool>) {
const exit = yield* run(args).pipe(Effect.exit)
if (Exit.isFailure(exit)) {
const err = Cause.squash(exit.cause)
return err instanceof Error ? err : new Error(String(err))
} }
} throw new Error("expected edit to fail")
})
const put = Effect.fn("EditToolTest.put")(function* (p: string, content: string) {
const fs = yield* AppFileSystem.Service
yield* fs.writeWithDirs(p, content)
})
const load = Effect.fn("EditToolTest.load")(function* (p: string) {
const fs = yield* AppFileSystem.Service
return yield* fs.readFileString(p)
})
const loadRaw = Effect.fn("EditToolTest.loadRaw")(function* (p: string) {
return yield* Effect.promise(() => fs.readFile(p, "utf-8"))
})
const makeDirectory = Effect.fn("EditToolTest.makeDirectory")(function* (p: string) {
const fs = yield* AppFileSystem.Service
yield* fs.makeDirectory(p)
})
const onceBus = Effect.fn("EditToolTest.onceBus")(function* (def: typeof FileWatcher.Event.Updated) {
const bus = yield* Bus.Service
const deferred = yield* Deferred.make<void>()
const unsub = yield* bus.subscribeCallback(def, () => Effect.runSync(Deferred.succeed(deferred, undefined)))
yield* Effect.addFinalizer(() => Effect.sync(unsub))
return deferred
})
describe("tool.edit", () => { describe("tool.edit", () => {
describe("creating new files", () => { describe("creating new files", () => {
test("creates new file when oldString is empty", async () => { it.instance("creates new file when oldString is empty", () =>
await using tmp = await tmpdir() Effect.gen(function* () {
const filepath = path.join(tmp.path, "newfile.txt") const test = yield* TestInstance
const filepath = path.join(test.directory, "newfile.txt")
await WithInstance.provide({ const result = yield* run({ filePath: filepath, oldString: "", newString: "new content" })
directory: tmp.path,
fn: async () => {
const edit = await resolve()
const result = await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "",
newString: "new content",
},
ctx,
),
)
expect(result.metadata.diff).toContain("new content") expect(result.metadata.diff).toContain("new content")
expect(yield* load(filepath)).toBe("new content")
const content = await fs.readFile(filepath, "utf-8") }),
expect(content).toBe("new content")
},
})
})
test("preserves BOM when oldString is empty on existing files", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "existing.cs")
const bom = String.fromCharCode(0xfeff)
await fs.writeFile(filepath, `${bom}using System;\n`, "utf-8")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
const result = await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "",
newString: "using Up;\n",
},
ctx,
),
) )
it.instance("preserves BOM when oldString is empty on existing files", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "existing.cs")
const bom = String.fromCharCode(0xfeff)
yield* put(filepath, `${bom}using System;\n`)
const result = yield* run({ filePath: filepath, oldString: "", newString: "using Up;\n" })
expect(result.metadata.diff).toContain("-using System;") expect(result.metadata.diff).toContain("-using System;")
expect(result.metadata.diff).toContain("+using Up;") expect(result.metadata.diff).toContain("+using Up;")
const content = await fs.readFile(filepath, "utf-8") const content = yield* loadRaw(filepath)
expect(content.charCodeAt(0)).toBe(0xfeff) expect(content.charCodeAt(0)).toBe(0xfeff)
expect(content.slice(1)).toBe("using Up;\n") expect(content.slice(1)).toBe("using Up;\n")
}, }),
})
})
test("creates new file with nested directories", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "nested", "dir", "file.txt")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "",
newString: "nested file",
},
ctx,
),
) )
const content = await fs.readFile(filepath, "utf-8") it.instance("creates new file with nested directories", () =>
expect(content).toBe("nested file") Effect.gen(function* () {
}, const test = yield* TestInstance
}) const filepath = path.join(test.directory, "nested", "dir", "file.txt")
})
test("emits add event for new files", async () => { yield* run({ filePath: filepath, oldString: "", newString: "nested file" })
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "new.txt")
await WithInstance.provide({ expect(yield* load(filepath)).toBe("nested file")
directory: tmp.path, }),
fn: async () => {
const { FileWatcher } = await import("../../src/file/watcher")
const updated = await onceBus(FileWatcher.Event.Updated)
try {
const edit = await resolve()
await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "",
newString: "content",
},
ctx,
),
) )
await updated.wait it.instance("emits add event for new files", () =>
} finally { Effect.gen(function* () {
updated.unsub() const test = yield* TestInstance
} const updated = yield* onceBus(FileWatcher.Event.Updated)
},
}) yield* run({ filePath: path.join(test.directory, "new.txt"), oldString: "", newString: "content" })
}) yield* Deferred.await(updated)
}),
)
}) })
describe("editing existing files", () => { describe("editing existing files", () => {
test("replaces text in existing file", async () => { it.instance("replaces text in existing file", () =>
await using tmp = await tmpdir() Effect.gen(function* () {
const filepath = path.join(tmp.path, "existing.txt") const test = yield* TestInstance
await fs.writeFile(filepath, "old content here", "utf-8") const filepath = path.join(test.directory, "existing.txt")
yield* put(filepath, "old content here")
await WithInstance.provide({ const result = yield* run({ filePath: filepath, oldString: "old content", newString: "new content" })
directory: tmp.path,
fn: async () => {
const edit = await resolve()
const result = await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "old content",
newString: "new content",
},
ctx,
),
)
expect(result.output).toContain("Edit applied successfully") expect(result.output).toContain("Edit applied successfully")
expect(yield* load(filepath)).toBe("new content here")
const content = await fs.readFile(filepath, "utf-8") }),
expect(content).toBe("new content here")
},
})
})
test("replaces the first visible line in BOM files", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "existing.cs")
const bom = String.fromCharCode(0xfeff)
await fs.writeFile(filepath, `${bom}using System;\nclass Test {}\n`, "utf-8")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
const result = await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "using System;",
newString: "using Up;",
},
ctx,
),
) )
it.instance("replaces the first visible line in BOM files", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "existing.cs")
const bom = String.fromCharCode(0xfeff)
yield* put(filepath, `${bom}using System;\nclass Test {}\n`)
const result = yield* run({ filePath: filepath, oldString: "using System;", newString: "using Up;" })
expect(result.metadata.diff).toContain("-using System;") expect(result.metadata.diff).toContain("-using System;")
expect(result.metadata.diff).toContain("+using Up;") expect(result.metadata.diff).toContain("+using Up;")
expect(result.metadata.diff).not.toContain(bom) expect(result.metadata.diff).not.toContain(bom)
const content = await fs.readFile(filepath, "utf-8") const content = yield* loadRaw(filepath)
expect(content.charCodeAt(0)).toBe(0xfeff) expect(content.charCodeAt(0)).toBe(0xfeff)
expect(content.slice(1)).toBe("using Up;\nclass Test {}\n") expect(content.slice(1)).toBe("using Up;\nclass Test {}\n")
}, }),
})
})
test("throws error when file does not exist", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "nonexistent.txt")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
await expect(
Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "old",
newString: "new",
},
ctx,
),
),
).rejects.toThrow("not found")
},
})
})
test("throws error when oldString equals newString", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "file.txt")
await fs.writeFile(filepath, "content", "utf-8")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
await expect(
Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "same",
newString: "same",
},
ctx,
),
),
).rejects.toThrow("identical")
},
})
})
test("throws error when oldString not found in file", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "file.txt")
await fs.writeFile(filepath, "actual content", "utf-8")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
await expect(
Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "not in file",
newString: "replacement",
},
ctx,
),
),
).rejects.toThrow()
},
})
})
test("replaces all occurrences with replaceAll option", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "file.txt")
await fs.writeFile(filepath, "foo bar foo baz foo", "utf-8")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "foo",
newString: "qux",
replaceAll: true,
},
ctx,
),
) )
const content = await fs.readFile(filepath, "utf-8") it.instance("throws error when file does not exist", () =>
expect(content).toBe("qux bar qux baz qux") Effect.gen(function* () {
}, const test = yield* TestInstance
}) expect(
}) (yield* fail({ filePath: path.join(test.directory, "nonexistent.txt"), oldString: "old", newString: "new" }))
.message,
test("emits change event for existing files", async () => { ).toContain("not found")
await using tmp = await tmpdir() }),
const filepath = path.join(tmp.path, "file.txt")
await fs.writeFile(filepath, "original", "utf-8")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const { FileWatcher } = await import("../../src/file/watcher")
const updated = await onceBus(FileWatcher.Event.Updated)
try {
const edit = await resolve()
await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "original",
newString: "modified",
},
ctx,
),
) )
await updated.wait it.instance("throws error when oldString equals newString", () =>
} finally { Effect.gen(function* () {
updated.unsub() const test = yield* TestInstance
} const filepath = path.join(test.directory, "file.txt")
}, yield* put(filepath, "content")
})
}) expect((yield* fail({ filePath: filepath, oldString: "same", newString: "same" })).message).toContain(
"identical",
)
}),
)
it.instance("throws error when oldString not found in file", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "file.txt")
yield* put(filepath, "actual content")
expect(yield* fail({ filePath: filepath, oldString: "not in file", newString: "replacement" })).toBeInstanceOf(
Error,
)
}),
)
it.instance("replaces all occurrences with replaceAll option", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "file.txt")
yield* put(filepath, "foo bar foo baz foo")
yield* run({ filePath: filepath, oldString: "foo", newString: "qux", replaceAll: true })
expect(yield* load(filepath)).toBe("qux bar qux baz qux")
}),
)
it.instance("emits change event for existing files", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "file.txt")
yield* put(filepath, "original")
const updated = yield* onceBus(FileWatcher.Event.Updated)
yield* run({ filePath: filepath, oldString: "original", newString: "modified" })
yield* Deferred.await(updated)
}),
)
}) })
describe("edge cases", () => { describe("edge cases", () => {
test("handles multiline replacements", async () => { it.instance("handles multiline replacements", () =>
await using tmp = await tmpdir() Effect.gen(function* () {
const filepath = path.join(tmp.path, "file.txt") const test = yield* TestInstance
await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8") const filepath = path.join(test.directory, "file.txt")
yield* put(filepath, "line1\nline2\nline3")
await WithInstance.provide({ yield* run({ filePath: filepath, oldString: "line2", newString: "new line 2\nextra line" })
directory: tmp.path,
fn: async () => { expect(yield* load(filepath)).toBe("line1\nnew line 2\nextra line\nline3")
const edit = await resolve() }),
await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "line2",
newString: "new line 2\nextra line",
},
ctx,
),
) )
const content = await fs.readFile(filepath, "utf-8") it.instance("handles CRLF line endings", () =>
expect(content).toBe("line1\nnew line 2\nextra line\nline3") Effect.gen(function* () {
}, const test = yield* TestInstance
}) const filepath = path.join(test.directory, "file.txt")
}) yield* put(filepath, "line1\r\nold\r\nline3")
test("handles CRLF line endings", async () => { yield* run({ filePath: filepath, oldString: "old", newString: "new" })
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "file.txt")
await fs.writeFile(filepath, "line1\r\nold\r\nline3", "utf-8")
await WithInstance.provide({ expect(yield* load(filepath)).toBe("line1\r\nnew\r\nline3")
directory: tmp.path, }),
fn: async () => {
const edit = await resolve()
await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "old",
newString: "new",
},
ctx,
),
) )
const content = await fs.readFile(filepath, "utf-8") it.instance("throws error when oldString equals newString", () =>
expect(content).toBe("line1\r\nnew\r\nline3") Effect.gen(function* () {
}, const test = yield* TestInstance
}) const filepath = path.join(test.directory, "file.txt")
}) yield* put(filepath, "content")
test("throws error when oldString equals newString", async () => { expect((yield* fail({ filePath: filepath, oldString: "", newString: "" })).message).toContain("identical")
await using tmp = await tmpdir() }),
const filepath = path.join(tmp.path, "file.txt")
await fs.writeFile(filepath, "content", "utf-8")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
await expect(
Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "",
newString: "",
},
ctx,
),
),
).rejects.toThrow("identical")
},
})
})
test("throws error when path is directory", async () => {
await using tmp = await tmpdir()
const dirpath = path.join(tmp.path, "adir")
await fs.mkdir(dirpath)
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
await expect(
Effect.runPromise(
edit.execute(
{
filePath: dirpath,
oldString: "old",
newString: "new",
},
ctx,
),
),
).rejects.toThrow("directory")
},
})
})
test("tracks file diff statistics", async () => {
await using tmp = await tmpdir()
const filepath = path.join(tmp.path, "file.txt")
await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8")
await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
const result = await Effect.runPromise(
edit.execute(
{
filePath: filepath,
oldString: "line2",
newString: "new line a\nnew line b",
},
ctx,
),
) )
it.instance("throws error when path is directory", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const dirpath = path.join(test.directory, "adir")
yield* makeDirectory(dirpath)
expect((yield* fail({ filePath: dirpath, oldString: "old", newString: "new" })).message).toContain("directory")
}),
)
it.instance("tracks file diff statistics", () =>
Effect.gen(function* () {
const test = yield* TestInstance
const filepath = path.join(test.directory, "file.txt")
yield* put(filepath, "line1\nline2\nline3")
const result = yield* run({ filePath: filepath, oldString: "line2", newString: "new line a\nnew line b" })
expect(result.metadata.filediff).toBeDefined() expect(result.metadata.filediff).toBeDefined()
expect(result.metadata.filediff.file).toBe(filepath) expect(result.metadata.filediff.file).toBe(filepath)
expect(result.metadata.filediff.additions).toBeGreaterThan(0) expect(result.metadata.filediff.additions).toBeGreaterThan(0)
}, }),
}) )
})
}) })
describe("line endings", () => { describe("line endings", () => {
@@ -552,127 +349,129 @@ describe("tool.edit", () => {
replaceAll?: boolean replaceAll?: boolean
} }
const apply = async (input: Input) => { const apply = Effect.fn("EditToolTest.lineEndings.apply")(function* (input: Input) {
await using tmp = await tmpdir({ const test = yield* TestInstance
init: async (dir) => { const filePath = path.join(test.directory, "test.txt")
await Bun.write(path.join(dir, "test.txt"), input.content) yield* put(filePath, input.content)
}, yield* run({
})
return await WithInstance.provide({
directory: tmp.path,
fn: async () => {
const edit = await resolve()
const filePath = path.join(tmp.path, "test.txt")
await Effect.runPromise(
edit.execute(
{
filePath, filePath,
oldString: input.oldString, oldString: input.oldString,
newString: input.newString, newString: input.newString,
replaceAll: input.replaceAll, replaceAll: input.replaceAll,
}, })
ctx, return yield* load(filePath)
), })
it.instance("preserves LF with LF multi-line strings", () =>
Effect.gen(function* () {
const content = normalize(old + "\n", "\n")
const output = yield* apply({
content,
oldString: normalize(old, "\n"),
newString: normalize(next, "\n"),
})
expect(output).toBe(normalize(next + "\n", "\n"))
expectLf(output)
}),
) )
return await Bun.file(filePath).text()
},
})
}
test("preserves LF with LF multi-line strings", async () => { it.instance("preserves CRLF with CRLF multi-line strings", () =>
const content = normalize(old + "\n", "\n") Effect.gen(function* () {
const output = await apply({
content,
oldString: normalize(old, "\n"),
newString: normalize(next, "\n"),
})
expect(output).toBe(normalize(next + "\n", "\n"))
expectLf(output)
})
test("preserves CRLF with CRLF multi-line strings", async () => {
const content = normalize(old + "\n", "\r\n") const content = normalize(old + "\n", "\r\n")
const output = await apply({ const output = yield* apply({
content, content,
oldString: normalize(old, "\r\n"), oldString: normalize(old, "\r\n"),
newString: normalize(next, "\r\n"), newString: normalize(next, "\r\n"),
}) })
expect(output).toBe(normalize(next + "\n", "\r\n")) expect(output).toBe(normalize(next + "\n", "\r\n"))
expectCrlf(output) expectCrlf(output)
}) }),
)
test("preserves LF when old/new use CRLF", async () => { it.instance("preserves LF when old/new use CRLF", () =>
Effect.gen(function* () {
const content = normalize(old + "\n", "\n") const content = normalize(old + "\n", "\n")
const output = await apply({ const output = yield* apply({
content, content,
oldString: normalize(old, "\r\n"), oldString: normalize(old, "\r\n"),
newString: normalize(next, "\r\n"), newString: normalize(next, "\r\n"),
}) })
expect(output).toBe(normalize(next + "\n", "\n")) expect(output).toBe(normalize(next + "\n", "\n"))
expectLf(output) expectLf(output)
}) }),
)
test("preserves CRLF when old/new use LF", async () => { it.instance("preserves CRLF when old/new use LF", () =>
Effect.gen(function* () {
const content = normalize(old + "\n", "\r\n") const content = normalize(old + "\n", "\r\n")
const output = await apply({ const output = yield* apply({
content, content,
oldString: normalize(old, "\n"), oldString: normalize(old, "\n"),
newString: normalize(next, "\n"), newString: normalize(next, "\n"),
}) })
expect(output).toBe(normalize(next + "\n", "\r\n")) expect(output).toBe(normalize(next + "\n", "\r\n"))
expectCrlf(output) expectCrlf(output)
}) }),
)
test("preserves LF when newString uses CRLF", async () => { it.instance("preserves LF when newString uses CRLF", () =>
Effect.gen(function* () {
const content = normalize(old + "\n", "\n") const content = normalize(old + "\n", "\n")
const output = await apply({ const output = yield* apply({
content, content,
oldString: normalize(old, "\n"), oldString: normalize(old, "\n"),
newString: normalize(next, "\r\n"), newString: normalize(next, "\r\n"),
}) })
expect(output).toBe(normalize(next + "\n", "\n")) expect(output).toBe(normalize(next + "\n", "\n"))
expectLf(output) expectLf(output)
}) }),
)
test("preserves CRLF when newString uses LF", async () => { it.instance("preserves CRLF when newString uses LF", () =>
Effect.gen(function* () {
const content = normalize(old + "\n", "\r\n") const content = normalize(old + "\n", "\r\n")
const output = await apply({ const output = yield* apply({
content, content,
oldString: normalize(old, "\r\n"), oldString: normalize(old, "\r\n"),
newString: normalize(next, "\n"), newString: normalize(next, "\n"),
}) })
expect(output).toBe(normalize(next + "\n", "\r\n")) expect(output).toBe(normalize(next + "\n", "\r\n"))
expectCrlf(output) expectCrlf(output)
}) }),
)
test("preserves LF with mixed old/new line endings", async () => { it.instance("preserves LF with mixed old/new line endings", () =>
Effect.gen(function* () {
const content = normalize(old + "\n", "\n") const content = normalize(old + "\n", "\n")
const output = await apply({ const output = yield* apply({
content, content,
oldString: "alpha\nbeta\r\ngamma", oldString: "alpha\nbeta\r\ngamma",
newString: "alpha\r\nbeta\nomega", newString: "alpha\r\nbeta\nomega",
}) })
expect(output).toBe(normalize(alt + "\n", "\n")) expect(output).toBe(normalize(alt + "\n", "\n"))
expectLf(output) expectLf(output)
}) }),
)
test("preserves CRLF with mixed old/new line endings", async () => { it.instance("preserves CRLF with mixed old/new line endings", () =>
Effect.gen(function* () {
const content = normalize(old + "\n", "\r\n") const content = normalize(old + "\n", "\r\n")
const output = await apply({ const output = yield* apply({
content, content,
oldString: "alpha\r\nbeta\ngamma", oldString: "alpha\r\nbeta\ngamma",
newString: "alpha\nbeta\r\nomega", newString: "alpha\nbeta\r\nomega",
}) })
expect(output).toBe(normalize(alt + "\n", "\r\n")) expect(output).toBe(normalize(alt + "\n", "\r\n"))
expectCrlf(output) expectCrlf(output)
}) }),
)
test("replaceAll preserves LF for multi-line blocks", async () => { it.instance("replaceAll preserves LF for multi-line blocks", () =>
Effect.gen(function* () {
const blockOld = "alpha\nbeta" const blockOld = "alpha\nbeta"
const blockNew = "alpha\nbeta-updated" const blockNew = "alpha\nbeta-updated"
const content = normalize(blockOld + "\n" + blockOld + "\n", "\n") const content = normalize(blockOld + "\n" + blockOld + "\n", "\n")
const output = await apply({ const output = yield* apply({
content, content,
oldString: normalize(blockOld, "\n"), oldString: normalize(blockOld, "\n"),
newString: normalize(blockNew, "\n"), newString: normalize(blockNew, "\n"),
@@ -680,13 +479,15 @@ describe("tool.edit", () => {
}) })
expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\n")) expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\n"))
expectLf(output) expectLf(output)
}) }),
)
test("replaceAll preserves CRLF for multi-line blocks", async () => { it.instance("replaceAll preserves CRLF for multi-line blocks", () =>
Effect.gen(function* () {
const blockOld = "alpha\nbeta" const blockOld = "alpha\nbeta"
const blockNew = "alpha\nbeta-updated" const blockNew = "alpha\nbeta-updated"
const content = normalize(blockOld + "\n" + blockOld + "\n", "\r\n") const content = normalize(blockOld + "\n" + blockOld + "\n", "\r\n")
const output = await apply({ const output = yield* apply({
content, content,
oldString: normalize(blockOld, "\r\n"), oldString: normalize(blockOld, "\r\n"),
newString: normalize(blockNew, "\r\n"), newString: normalize(blockNew, "\r\n"),
@@ -694,7 +495,8 @@ describe("tool.edit", () => {
}) })
expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\r\n")) expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\r\n"))
expectCrlf(output) expectCrlf(output)
}) }),
)
}) })
describe("concurrent editing", () => { describe("concurrent editing", () => {