openAgent
gitlabThe open source UNIVERSAL agent.
clone
https://gitlab.com/bitzzdev/openAgent.gitAIagentautomationopen-sourceuniversal-agent
import fs from "fs/promises"import path from "path"import { fileURLToPath } from "url"import { describe, expect, test } from "bun:test"import { Effect, Layer } from "effect"import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"import { LayerNode } from "@opencode-ai/core/effect/layer-node"import { FileMutation } from "@opencode-ai/core/file-mutation"import { FSUtil } from "@opencode-ai/core/fs-util"import { Location } from "@opencode-ai/core/location"import { LocationMutation } from "@opencode-ai/core/location-mutation"import { PermissionV2 } from "@opencode-ai/core/permission"import { AbsolutePath } from "@opencode-ai/core/schema"import { SessionV2 } from "@opencode-ai/core/session"import { ToolRegistry } from "@opencode-ai/core/tool/registry"import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"import { EditTool } from "@opencode-ai/core/tool/edit"import { location } from "./fixture/location"import { tmpdir } from "./fixture/tmpdir"import { testEffect } from "./lib/effect"import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"const sessionID = SessionV2.ID.make("ses_edit_tool_test")const assertions: PermissionV2.AssertInput[] = []const writes: string[] = []let reads = 0let denyAction: string | undefinedlet afterRead = (_target: string, _content: Uint8Array): Effect.Effect<void> => Effect.voidconst permission = Layer.succeed(PermissionV2.Service,PermissionV2.Service.of({assert: (input) =>Effect.sync(() => assertions.push(input)).pipe(Effect.andThen(input.action === denyAction ? Effect.fail(new PermissionV2.BlockedError({ rules: [] })) : Effect.void,),),ask: () => Effect.die("unused"),reply: () => Effect.die("unused"),get: () => Effect.die("unused"),forSession: () => Effect.die("unused"),list: () => Effect.die("unused"),}),)const reset = () => {assertions.length = 0writes.length = 0reads = 0denyAction = undefinedafterRead = () => Effect.void}const filesystem = Layer.effect(FSUtil.Service,Effect.gen(function* () {const fs = yield* FSUtil.Servicereturn FSUtil.Service.of({...fs,readFile: (target) =>fs.readFile(target).pipe(Effect.tap((content) =>Effect.sync(() => reads++).pipe(Effect.andThen(Effect.suspend(() => afterRead(target, content)))),),),writeWithDirs: (target, content, mode) =>Effect.sync(() => writes.push(target)).pipe(Effect.andThen(fs.writeWithDirs(target, content, mode))),writeFile: (target, content, options) =>Effect.sync(() => writes.push(target)).pipe(Effect.andThen(fs.writeFile(target, content, options))),writeFileString: (target, content, options) =>Effect.sync(() => writes.push(target)).pipe(Effect.andThen(fs.writeFileString(target, content, options))),})}),).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))const withTool = <A, E, R>(directory: string, body: (registry: ToolRegistry.Interface) => Effect.Effect<A, E, R>) => {const activeLocation = Layer.succeed(Location.Service,Location.Service.of(location({ directory: AbsolutePath.make(directory) })),)return Effect.gen(function* () {return yield* body(yield* ToolRegistry.Service)}).pipe(Effect.provide(AppNodeBuilder.build(LayerNode.group([ToolRegistry.node,ToolRegistry.toolsNode,LocationMutation.node,FileMutation.node,EditTool.node,]),[[FSUtil.node, filesystem],[Location.node, activeLocation],[PermissionV2.node, permission],[ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],],),),)}const call = (input: typeof EditTool.Input.Type, id = "call-edit") => ({sessionID,...toolIdentity,call: { type: "tool-call" as const, id, name: "edit", input },})const it = testEffect(Layer.empty)describe("EditTool", () => {it.live("registers and replaces relative exact text through FileMutation once", () =>Effect.acquireUseRelease(Effect.promise(() => tmpdir()),(tmp) => {reset()const target = path.join(tmp.path, "hello.txt")return Effect.promise(() => fs.writeFile(target, "before\nrest\n")).pipe(Effect.andThen(withTool(tmp.path, (registry) =>Effect.gen(function* () {expect((yield* toolDefinitions(registry)).map((tool) => tool.name)).toEqual(["edit"])expect(yield* toolDefinitions(registry, [{ action: "edit", resource: "*", effect: "deny" }])).toEqual([],)const settled = yield* settleTool(registry,call({ path: "hello.txt", oldString: "before", newString: "after" }),)expect(settled.result).toEqual({type: "text",value: "Edited file successfully: hello.txt\nReplacements: 1\n```diff\n-before\n+after\n```",})expect(settled.output?.structured).toEqual({replacements: 1,files: [{file: "hello.txt",status: "modified",additions: 1,deletions: 1,patch: expect.stringContaining("-before\n+after"),},],})expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after\nrest\n")expect(assertions).toMatchObject([{ sessionID, action: "edit", resources: ["hello.txt"], save: ["*"] }])expect(writes).toEqual([yield* Effect.promise(() => fs.realpath(target))])}),),),)},(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),),)it.live("accepts an absolute file path inside the active Location", () =>Effect.acquireUseRelease(Effect.promise(() => tmpdir()),(tmp) => {reset()const target = path.join(tmp.path, "absolute.txt")return Effect.promise(() => fs.writeFile(target, "before")).pipe(Effect.andThen(withTool(tmp.path, (registry) =>executeTool(registry, call({ path: target, oldString: "before", newString: "after" })),),),Effect.andThen((result) =>Effect.gen(function* () {expect(result.type).toBe("text")expect(assertions.map((input) => input.action)).toEqual(["edit"])expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after")}),),)},(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),),)it.live("approves an explicit external absolute path before edit", () =>Effect.acquireUseRelease(Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),([active, outside]) => {reset()const target = path.join(outside.path, "external.txt")return Effect.promise(() => fs.writeFile(target, "before")).pipe(Effect.andThen(withTool(active.path, (registry) =>executeTool(registry, call({ path: target, oldString: "before", newString: "after" })),),),Effect.andThen((result) =>Effect.gen(function* () {expect(result.type).toBe("text")expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after")expect(writes).toHaveLength(1)}),),)},([active, outside]) =>Effect.promise(() =>Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined),),),)it.live("does not write when external_directory or edit approval is denied", () =>Effect.acquireUseRelease(Effect.promise(() => Promise.all([tmpdir(), tmpdir()])),([active, outside]) =>Effect.gen(function* () {const external = path.join(outside.path, "denied.txt")yield* Effect.promise(() => fs.writeFile(external, "before"))reset()denyAction = "external_directory"expect(yield* withTool(active.path, (registry) =>executeTool(registry, call({ path: external, oldString: "before", newString: "after" })),),).toEqual({type: "error",value: `Unable to edit ${external}`,})expect(assertions.map((input) => input.action)).toEqual(["external_directory"])expect(reads).toBe(0)expect(writes).toEqual([])reset()denyAction = "edit"expect(yield* withTool(active.path, (registry) =>executeTool(registry, call({ path: external, oldString: "before", newString: "after" })),),).toEqual({type: "error",value: `Unable to edit ${external}`,})expect(assertions.map((input) => input.action)).toEqual(["external_directory", "edit"])expect(reads).toBe(0)expect(writes).toEqual([])expect(yield* Effect.promise(() => fs.readFile(external, "utf8"))).toBe("before")}),([active, outside]) =>Effect.promise(() =>Promise.all([active[Symbol.asyncDispose](), outside[Symbol.asyncDispose]()]).then(() => undefined),),),)it.live("denied edit reads no target content and does not disclose whether oldString matches", () =>Effect.acquireUseRelease(Effect.promise(() => tmpdir()),(tmp) => {reset()denyAction = "edit"const target = path.join(tmp.path, "secret.txt")return Effect.promise(() => fs.writeFile(target, "secret content")).pipe(Effect.andThen(withTool(tmp.path, (registry) =>Effect.gen(function* () {const matching = yield* executeTool(registry,call({ path: "secret.txt", oldString: "secret content", newString: "replacement" }),)const missing = yield* executeTool(registry,call({ path: "secret.txt", oldString: "not present", newString: "replacement" }),)expect(matching).toEqual({ type: "error", value: "Unable to edit secret.txt" })expect(missing).toEqual(matching)expect(assertions.map((input) => input.action)).toEqual(["edit", "edit"])expect(reads).toBe(0)expect(writes).toEqual([])}),),),)},(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),),)it.live("rejects no-op, empty, missing, and ambiguous exact replacements", () =>Effect.acquireUseRelease(Effect.promise(() => tmpdir()),(tmp) => {reset()const target = path.join(tmp.path, "matches.txt")return Effect.promise(() => fs.writeFile(target, "same same")).pipe(Effect.andThen(withTool(tmp.path, (registry) =>Effect.gen(function* () {expect(yield* executeTool(registry, call({ path: "matches.txt", oldString: "same", newString: "same" })),).toEqual({type: "error",value: "No changes to apply: oldString and newString are identical.",})expect(yield* executeTool(registry, call({ path: "matches.txt", oldString: "", newString: "after" })),).toEqual({type: "error",value: "oldString must not be empty. Use write to create or overwrite a file.",})expect(yield* executeTool(registry, call({ path: "matches.txt", oldString: "missing", newString: "after" })),).toEqual({type: "error",value:"Could not find oldString in the file. It must match exactly, including whitespace and indentation.",})expect(yield* executeTool(registry, call({ path: "matches.txt", oldString: "same", newString: "after" })),).toEqual({type: "error",value:"Found multiple exact matches for oldString. Provide more surrounding context or set replaceAll to true.",})expect(writes).toEqual([])}),),),)},(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),),)it.live("replaces every exact occurrence when replaceAll is true", () =>Effect.acquireUseRelease(Effect.promise(() => tmpdir()),(tmp) => {reset()const target = path.join(tmp.path, "all.txt")return Effect.promise(() => fs.writeFile(target, "same same same")).pipe(Effect.andThen(withTool(tmp.path, (registry) =>settleTool(registry, call({ path: "all.txt", oldString: "same", newString: "after", replaceAll: true })),),),Effect.andThen((settled) =>Effect.gen(function* () {expect(settled.output?.structured).toMatchObject({ replacements: 3 })expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("after after after")expect(writes).toHaveLength(1)}),),)},(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),),)it.live("preserves BOM and CRLF line endings", () =>Effect.acquireUseRelease(Effect.promise(() => tmpdir()),(tmp) => {reset()const target = path.join(tmp.path, "windows.txt")return Effect.promise(() => fs.writeFile(target, "\uFEFFbefore\r\nrest\r\n")).pipe(Effect.andThen(withTool(tmp.path, (registry) =>executeTool(registry, call({ path: "windows.txt", oldString: "before\nrest", newString: "after\nrest" })),),),Effect.andThen(() => Effect.promise(() => fs.readFile(target, "utf8"))),Effect.tap((content) => Effect.sync(() => expect(content).toBe("\uFEFFafter\r\nrest\r\n"))),)},(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),),)it.live("rejects an in-place content change after matching but before conditional commit", () =>Effect.acquireUseRelease(Effect.promise(() => tmpdir()),(tmp) => {reset()const target = path.join(tmp.path, "concurrent.txt")afterRead = () => (reads === 1 ? Effect.promise(() => fs.writeFile(target, "newer\n")) : Effect.void)return Effect.promise(() => fs.writeFile(target, "before\n")).pipe(Effect.andThen(withTool(tmp.path, (registry) =>executeTool(registry, call({ path: "concurrent.txt", oldString: "before", newString: "after" })),),),Effect.andThen((result) =>Effect.gen(function* () {expect(result).toEqual({type: "error",value: "File changed after permission approval. Read it again before editing.",})expect(yield* Effect.promise(() => fs.readFile(target, "utf8"))).toBe("newer\n")expect(writes).toEqual([])}),),)},(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),),)})test("keeps the locked edit schema, semantics docstring, and deferred TODOs visible", async () => {const source = (await fs.readFile(new URL("../src/tool/edit.ts", import.meta.url), "utf8")).replaceAll("\r\n", "\n")const definition = await Effect.runPromise(withTool(path.dirname(fileURLToPath(import.meta.url)), (registry) => toolDefinitions(registry)),)const schema = definition[0]?.inputSchema as { readonly properties?: Record<string, unknown> }expect(Object.keys(schema.properties ?? {}).sort()).toEqual(["newString", "oldString", "path", "replaceAll"])expect(source).toContain("absolute external paths retain mutation capability through a separate\n * external_directory approval before edit approval.",)for (const todo of ["Port V1 fuzzy correction strategies only after exact-edit behavior is established: line-trimmed matching, block-anchor fallback, indentation correction, and similarity-threshold review.","Add formatter integration after V2 formatter runtime exists.","Publish watcher/file-edit events after V2 watcher integration exists.","Add snapshots / undo after design exists.","Add LSP notification and diagnostics after V2 LSP runtime exists.",]) {expect(source).toContain(`TODO: ${todo}`)}})