From ce720207500f66127629d1e9f7209324d8b6370f Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 11 May 2026 21:42:17 -0400 Subject: [PATCH] test(tool): migrate edit tests to Effect runner (#26977) --- packages/opencode/test/tool/edit.test.ts | 910 +++++++++-------------- 1 file changed, 356 insertions(+), 554 deletions(-) diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index a629ff07d1..572fcd9aa4 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -1,19 +1,20 @@ import { afterAll, afterEach, describe, test, expect } from "bun:test" import path from "path" 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 { Instance } from "../../src/project/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 { AppFileSystem } from "@opencode-ai/core/filesystem" import { Format } from "../../src/format" import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" -import { BusEvent } from "../../src/bus/bus-event" import { Truncate } from "@/tool/truncate" 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 = { sessionID: SessionID.make("ses_test-edit-session"), @@ -30,17 +31,19 @@ afterEach(async () => { await disposeAllInstances() }) -const runtime = ManagedRuntime.make( - Layer.mergeAll( - LSP.defaultLayer, - AppFileSystem.defaultLayer, - Format.defaultLayer, - Bus.layer, - Truncate.defaultLayer, - Agent.defaultLayer, - ), +const layer = Layer.mergeAll( + LSP.defaultLayer, + AppFileSystem.defaultLayer, + Format.defaultLayer, + Bus.layer, + Truncate.defaultLayer, + Agent.defaultLayer, ) +const it = testEffect(layer) + +const runtime = ManagedRuntime.make(layer) + afterAll(async () => { await runtime.dispose() }) @@ -53,464 +56,258 @@ const resolve = () => }), ) -const subscribeBus = (def: D, callback: () => unknown) => - runtime.runPromise(Bus.Service.use((bus) => bus.subscribeCallback(def, callback))) +const init = Effect.fn("EditToolTest.init")(function* () { + const info = yield* EditTool + return yield* info.init() +}) -async function onceBus(def: D) { - const result = Promise.withResolvers() - const unsub = await subscribeBus(def, () => { - unsub() - result.resolve() - }) - return { - wait: result.promise, - unsub, +const run = Effect.fn("EditToolTest.run")(function* ( + args: Tool.InferParameters, + next: Tool.Context = ctx, +) { + const tool = yield* init() + return yield* tool.execute(args, next) +}) + +const fail = Effect.fn("EditToolTest.fail")(function* (args: Tool.InferParameters) { + 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() + const unsub = yield* bus.subscribeCallback(def, () => Effect.runSync(Deferred.succeed(deferred, undefined))) + yield* Effect.addFinalizer(() => Effect.sync(unsub)) + return deferred +}) describe("tool.edit", () => { describe("creating new files", () => { - test("creates new file when oldString is empty", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "newfile.txt") + it.instance("creates new file when oldString is empty", () => + Effect.gen(function* () { + const test = yield* TestInstance + const filepath = path.join(test.directory, "newfile.txt") + const result = yield* run({ filePath: filepath, oldString: "", newString: "new content" }) - await WithInstance.provide({ - 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(yield* load(filepath)).toBe("new content") + }), + ) - expect(result.metadata.diff).toContain("new content") + 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 content = await fs.readFile(filepath, "utf-8") - expect(content).toBe("new content") - }, - }) - }) + const result = yield* run({ filePath: filepath, oldString: "", newString: "using Up;\n" }) - 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") + expect(result.metadata.diff).toContain("-using System;") + expect(result.metadata.diff).toContain("+using Up;") - 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, - ), - ) + const content = yield* loadRaw(filepath) + expect(content.charCodeAt(0)).toBe(0xfeff) + expect(content.slice(1)).toBe("using Up;\n") + }), + ) - expect(result.metadata.diff).toContain("-using System;") - expect(result.metadata.diff).toContain("+using Up;") + it.instance("creates new file with nested directories", () => + Effect.gen(function* () { + const test = yield* TestInstance + const filepath = path.join(test.directory, "nested", "dir", "file.txt") - const content = await fs.readFile(filepath, "utf-8") - expect(content.charCodeAt(0)).toBe(0xfeff) - expect(content.slice(1)).toBe("using Up;\n") - }, - }) - }) + yield* run({ filePath: filepath, oldString: "", newString: "nested file" }) - test("creates new file with nested directories", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "nested", "dir", "file.txt") + expect(yield* load(filepath)).toBe("nested file") + }), + ) - await WithInstance.provide({ - directory: tmp.path, - fn: async () => { - const edit = await resolve() - await Effect.runPromise( - edit.execute( - { - filePath: filepath, - oldString: "", - newString: "nested file", - }, - ctx, - ), - ) + it.instance("emits add event for new files", () => + Effect.gen(function* () { + const test = yield* TestInstance + const updated = yield* onceBus(FileWatcher.Event.Updated) - const content = await fs.readFile(filepath, "utf-8") - expect(content).toBe("nested file") - }, - }) - }) - - test("emits add event for new files", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "new.txt") - - 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: "", - newString: "content", - }, - ctx, - ), - ) - - await updated.wait - } finally { - updated.unsub() - } - }, - }) - }) + yield* run({ filePath: path.join(test.directory, "new.txt"), oldString: "", newString: "content" }) + yield* Deferred.await(updated) + }), + ) }) describe("editing existing files", () => { - test("replaces text in existing file", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "existing.txt") - await fs.writeFile(filepath, "old content here", "utf-8") + it.instance("replaces text in existing file", () => + Effect.gen(function* () { + const test = yield* TestInstance + const filepath = path.join(test.directory, "existing.txt") + yield* put(filepath, "old content here") - await WithInstance.provide({ - 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, - ), - ) + const result = yield* run({ filePath: filepath, oldString: "old content", newString: "new content" }) - 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") - }, - }) - }) + 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`) - 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") + const result = yield* run({ filePath: filepath, oldString: "using System;", newString: "using Up;" }) - 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, - ), - ) + expect(result.metadata.diff).toContain("-using System;") + expect(result.metadata.diff).toContain("+using Up;") + expect(result.metadata.diff).not.toContain(bom) - expect(result.metadata.diff).toContain("-using System;") - expect(result.metadata.diff).toContain("+using Up;") - expect(result.metadata.diff).not.toContain(bom) + const content = yield* loadRaw(filepath) + expect(content.charCodeAt(0)).toBe(0xfeff) + expect(content.slice(1)).toBe("using Up;\nclass Test {}\n") + }), + ) - const content = await fs.readFile(filepath, "utf-8") - expect(content.charCodeAt(0)).toBe(0xfeff) - expect(content.slice(1)).toBe("using Up;\nclass Test {}\n") - }, - }) - }) + it.instance("throws error when file does not exist", () => + Effect.gen(function* () { + const test = yield* TestInstance + expect( + (yield* fail({ filePath: path.join(test.directory, "nonexistent.txt"), oldString: "old", newString: "new" })) + .message, + ).toContain("not found") + }), + ) - test("throws error when file does not exist", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "nonexistent.txt") + it.instance("throws error when oldString equals newString", () => + Effect.gen(function* () { + const test = yield* TestInstance + const filepath = path.join(test.directory, "file.txt") + yield* put(filepath, "content") - 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") - }, - }) - }) + expect((yield* fail({ filePath: filepath, oldString: "same", newString: "same" })).message).toContain( + "identical", + ) + }), + ) - 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") + 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") - 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") - }, - }) - }) + expect(yield* fail({ filePath: filepath, oldString: "not in file", newString: "replacement" })).toBeInstanceOf( + Error, + ) + }), + ) - 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") + 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") - 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() - }, - }) - }) + yield* run({ filePath: filepath, oldString: "foo", newString: "qux", replaceAll: true }) - 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") + expect(yield* load(filepath)).toBe("qux bar qux baz qux") + }), + ) - 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, - ), - ) + 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) - const content = await fs.readFile(filepath, "utf-8") - expect(content).toBe("qux bar qux baz qux") - }, - }) - }) - - test("emits change event for existing files", async () => { - 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 - } finally { - updated.unsub() - } - }, - }) - }) + yield* run({ filePath: filepath, oldString: "original", newString: "modified" }) + yield* Deferred.await(updated) + }), + ) }) describe("edge cases", () => { - test("handles multiline replacements", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "file.txt") - await fs.writeFile(filepath, "line1\nline2\nline3", "utf-8") + it.instance("handles multiline replacements", () => + Effect.gen(function* () { + const test = yield* TestInstance + const filepath = path.join(test.directory, "file.txt") + yield* put(filepath, "line1\nline2\nline3") - await WithInstance.provide({ - directory: tmp.path, - fn: async () => { - const edit = await resolve() - await Effect.runPromise( - edit.execute( - { - filePath: filepath, - oldString: "line2", - newString: "new line 2\nextra line", - }, - ctx, - ), - ) + yield* run({ filePath: filepath, oldString: "line2", newString: "new line 2\nextra line" }) - const content = await fs.readFile(filepath, "utf-8") - expect(content).toBe("line1\nnew line 2\nextra line\nline3") - }, - }) - }) + expect(yield* load(filepath)).toBe("line1\nnew line 2\nextra line\nline3") + }), + ) - test("handles CRLF line endings", async () => { - await using tmp = await tmpdir() - const filepath = path.join(tmp.path, "file.txt") - await fs.writeFile(filepath, "line1\r\nold\r\nline3", "utf-8") + it.instance("handles CRLF line endings", () => + Effect.gen(function* () { + const test = yield* TestInstance + const filepath = path.join(test.directory, "file.txt") + yield* put(filepath, "line1\r\nold\r\nline3") - await WithInstance.provide({ - directory: tmp.path, - fn: async () => { - const edit = await resolve() - await Effect.runPromise( - edit.execute( - { - filePath: filepath, - oldString: "old", - newString: "new", - }, - ctx, - ), - ) + yield* run({ filePath: filepath, oldString: "old", newString: "new" }) - const content = await fs.readFile(filepath, "utf-8") - expect(content).toBe("line1\r\nnew\r\nline3") - }, - }) - }) + expect(yield* load(filepath)).toBe("line1\r\nnew\r\nline3") + }), + ) - 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") + it.instance("throws error when oldString equals newString", () => + Effect.gen(function* () { + const test = yield* TestInstance + const filepath = path.join(test.directory, "file.txt") + yield* put(filepath, "content") - 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") - }, - }) - }) + expect((yield* fail({ filePath: filepath, oldString: "", newString: "" })).message).toContain("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) + 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) - 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") - }, - }) - }) + expect((yield* fail({ filePath: dirpath, oldString: "old", newString: "new" })).message).toContain("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") + 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") - 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, - ), - ) + const result = yield* run({ filePath: filepath, oldString: "line2", newString: "new line a\nnew line b" }) - expect(result.metadata.filediff).toBeDefined() - expect(result.metadata.filediff.file).toBe(filepath) - expect(result.metadata.filediff.additions).toBeGreaterThan(0) - }, - }) - }) + expect(result.metadata.filediff).toBeDefined() + expect(result.metadata.filediff.file).toBe(filepath) + expect(result.metadata.filediff.additions).toBeGreaterThan(0) + }), + ) }) describe("line endings", () => { @@ -552,149 +349,154 @@ describe("tool.edit", () => { replaceAll?: boolean } - const apply = async (input: Input) => { - await using tmp = await tmpdir({ - init: async (dir) => { - await Bun.write(path.join(dir, "test.txt"), input.content) - }, + const apply = Effect.fn("EditToolTest.lineEndings.apply")(function* (input: Input) { + const test = yield* TestInstance + const filePath = path.join(test.directory, "test.txt") + yield* put(filePath, input.content) + yield* run({ + filePath, + oldString: input.oldString, + newString: input.newString, + replaceAll: input.replaceAll, }) - - 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, - oldString: input.oldString, - newString: input.newString, - replaceAll: input.replaceAll, - }, - ctx, - ), - ) - return await Bun.file(filePath).text() - }, - }) - } - - test("preserves LF with LF multi-line strings", async () => { - const content = normalize(old + "\n", "\n") - const output = await apply({ - content, - oldString: normalize(old, "\n"), - newString: normalize(next, "\n"), - }) - expect(output).toBe(normalize(next + "\n", "\n")) - expectLf(output) + return yield* load(filePath) }) - test("preserves CRLF with CRLF multi-line strings", async () => { - const content = normalize(old + "\n", "\r\n") - const output = await apply({ - content, - oldString: normalize(old, "\r\n"), - newString: normalize(next, "\r\n"), - }) - expect(output).toBe(normalize(next + "\n", "\r\n")) - expectCrlf(output) - }) + 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) + }), + ) - test("preserves LF when old/new use CRLF", async () => { - const content = normalize(old + "\n", "\n") - const output = await apply({ - content, - oldString: normalize(old, "\r\n"), - newString: normalize(next, "\r\n"), - }) - expect(output).toBe(normalize(next + "\n", "\n")) - expectLf(output) - }) + it.instance("preserves CRLF with CRLF multi-line strings", () => + Effect.gen(function* () { + const content = normalize(old + "\n", "\r\n") + const output = yield* apply({ + content, + oldString: normalize(old, "\r\n"), + newString: normalize(next, "\r\n"), + }) + expect(output).toBe(normalize(next + "\n", "\r\n")) + expectCrlf(output) + }), + ) - test("preserves CRLF when old/new use LF", async () => { - const content = normalize(old + "\n", "\r\n") - const output = await apply({ - content, - oldString: normalize(old, "\n"), - newString: normalize(next, "\n"), - }) - expect(output).toBe(normalize(next + "\n", "\r\n")) - expectCrlf(output) - }) + it.instance("preserves LF when old/new use CRLF", () => + Effect.gen(function* () { + const content = normalize(old + "\n", "\n") + const output = yield* apply({ + content, + oldString: normalize(old, "\r\n"), + newString: normalize(next, "\r\n"), + }) + expect(output).toBe(normalize(next + "\n", "\n")) + expectLf(output) + }), + ) - test("preserves LF when newString uses CRLF", async () => { - const content = normalize(old + "\n", "\n") - const output = await apply({ - content, - oldString: normalize(old, "\n"), - newString: normalize(next, "\r\n"), - }) - expect(output).toBe(normalize(next + "\n", "\n")) - expectLf(output) - }) + it.instance("preserves CRLF when old/new use LF", () => + Effect.gen(function* () { + const content = normalize(old + "\n", "\r\n") + const output = yield* apply({ + content, + oldString: normalize(old, "\n"), + newString: normalize(next, "\n"), + }) + expect(output).toBe(normalize(next + "\n", "\r\n")) + expectCrlf(output) + }), + ) - test("preserves CRLF when newString uses LF", async () => { - const content = normalize(old + "\n", "\r\n") - const output = await apply({ - content, - oldString: normalize(old, "\r\n"), - newString: normalize(next, "\n"), - }) - expect(output).toBe(normalize(next + "\n", "\r\n")) - expectCrlf(output) - }) + it.instance("preserves LF when newString uses CRLF", () => + Effect.gen(function* () { + const content = normalize(old + "\n", "\n") + const output = yield* apply({ + content, + oldString: normalize(old, "\n"), + newString: normalize(next, "\r\n"), + }) + expect(output).toBe(normalize(next + "\n", "\n")) + expectLf(output) + }), + ) - test("preserves LF with mixed old/new line endings", async () => { - const content = normalize(old + "\n", "\n") - const output = await apply({ - content, - oldString: "alpha\nbeta\r\ngamma", - newString: "alpha\r\nbeta\nomega", - }) - expect(output).toBe(normalize(alt + "\n", "\n")) - expectLf(output) - }) + it.instance("preserves CRLF when newString uses LF", () => + Effect.gen(function* () { + const content = normalize(old + "\n", "\r\n") + const output = yield* apply({ + content, + oldString: normalize(old, "\r\n"), + newString: normalize(next, "\n"), + }) + expect(output).toBe(normalize(next + "\n", "\r\n")) + expectCrlf(output) + }), + ) - test("preserves CRLF with mixed old/new line endings", async () => { - const content = normalize(old + "\n", "\r\n") - const output = await apply({ - content, - oldString: "alpha\r\nbeta\ngamma", - newString: "alpha\nbeta\r\nomega", - }) - expect(output).toBe(normalize(alt + "\n", "\r\n")) - expectCrlf(output) - }) + it.instance("preserves LF with mixed old/new line endings", () => + Effect.gen(function* () { + const content = normalize(old + "\n", "\n") + const output = yield* apply({ + content, + oldString: "alpha\nbeta\r\ngamma", + newString: "alpha\r\nbeta\nomega", + }) + expect(output).toBe(normalize(alt + "\n", "\n")) + expectLf(output) + }), + ) - test("replaceAll preserves LF for multi-line blocks", async () => { - const blockOld = "alpha\nbeta" - const blockNew = "alpha\nbeta-updated" - const content = normalize(blockOld + "\n" + blockOld + "\n", "\n") - const output = await apply({ - content, - oldString: normalize(blockOld, "\n"), - newString: normalize(blockNew, "\n"), - replaceAll: true, - }) - expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\n")) - expectLf(output) - }) + it.instance("preserves CRLF with mixed old/new line endings", () => + Effect.gen(function* () { + const content = normalize(old + "\n", "\r\n") + const output = yield* apply({ + content, + oldString: "alpha\r\nbeta\ngamma", + newString: "alpha\nbeta\r\nomega", + }) + expect(output).toBe(normalize(alt + "\n", "\r\n")) + expectCrlf(output) + }), + ) - test("replaceAll preserves CRLF for multi-line blocks", async () => { - const blockOld = "alpha\nbeta" - const blockNew = "alpha\nbeta-updated" - const content = normalize(blockOld + "\n" + blockOld + "\n", "\r\n") - const output = await apply({ - content, - oldString: normalize(blockOld, "\r\n"), - newString: normalize(blockNew, "\r\n"), - replaceAll: true, - }) - expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\r\n")) - expectCrlf(output) - }) + it.instance("replaceAll preserves LF for multi-line blocks", () => + Effect.gen(function* () { + const blockOld = "alpha\nbeta" + const blockNew = "alpha\nbeta-updated" + const content = normalize(blockOld + "\n" + blockOld + "\n", "\n") + const output = yield* apply({ + content, + oldString: normalize(blockOld, "\n"), + newString: normalize(blockNew, "\n"), + replaceAll: true, + }) + expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\n")) + expectLf(output) + }), + ) + + it.instance("replaceAll preserves CRLF for multi-line blocks", () => + Effect.gen(function* () { + const blockOld = "alpha\nbeta" + const blockNew = "alpha\nbeta-updated" + const content = normalize(blockOld + "\n" + blockOld + "\n", "\r\n") + const output = yield* apply({ + content, + oldString: normalize(blockOld, "\r\n"), + newString: normalize(blockNew, "\r\n"), + replaceAll: true, + }) + expect(output).toBe(normalize(blockNew + "\n" + blockNew + "\n", "\r\n")) + expectCrlf(output) + }), + ) }) describe("concurrent editing", () => {