diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 6c0c55e2b4..fabcfdcf6f 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -106,8 +106,7 @@ export namespace SessionPrompt { const run = { promise: (effect: Effect.Effect) => Effect.runPromise(effect.pipe(Effect.provide(EffectLogger.layer))), - fork: (effect: Effect.Effect) => - Effect.runFork(effect.pipe(Effect.provide(EffectLogger.layer))), + fork: (effect: Effect.Effect) => Effect.runFork(effect.pipe(Effect.provide(EffectLogger.layer))), } const cancel = Effect.fn("SessionPrompt.cancel")(function* (sessionID: SessionID) { @@ -622,23 +621,24 @@ NOTE: At any point in time through this workflow you should feel free to ask the }), Effect.onInterrupt(() => Effect.gen(function* () { - taskAbort.abort() - assistantMessage.finish = "tool-calls" - assistantMessage.time.completed = Date.now() - yield* sessions.updateMessage(assistantMessage) - if (part.state.status === "running") { - yield* sessions.updatePart({ - ...part, - state: { - status: "error", - error: "Cancelled", - time: { start: part.state.time.start, end: Date.now() }, - metadata: part.state.metadata, - input: part.state.input, - }, - } satisfies MessageV2.ToolPart) - } - })), + taskAbort.abort() + assistantMessage.finish = "tool-calls" + assistantMessage.time.completed = Date.now() + yield* sessions.updateMessage(assistantMessage) + if (part.state.status === "running") { + yield* sessions.updatePart({ + ...part, + state: { + status: "error", + error: "Cancelled", + time: { start: part.state.time.start, end: Date.now() }, + metadata: part.state.metadata, + input: part.state.input, + }, + } satisfies MessageV2.ToolPart) + } + }), + ), ) const attachments = result?.attachments?.map((attachment) => ({ diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index 8ce1c5ecac..53b5f61ffe 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -11,7 +11,9 @@ import { Bus } from "../../src/bus" import { tmpdir } from "../fixture/fixture" import { SessionID, MessageID } from "../../src/session/schema" -const runtime = ManagedRuntime.make(Layer.mergeAll(LSP.defaultLayer, AppFileSystem.defaultLayer, Format.defaultLayer, Bus.layer)) +const runtime = ManagedRuntime.make( + Layer.mergeAll(LSP.defaultLayer, AppFileSystem.defaultLayer, Format.defaultLayer, Bus.layer), +) const baseCtx = { sessionID: SessionID.make("ses_test"), @@ -57,7 +59,9 @@ const makeCtx = () => { const ctx: ToolCtx = { ...baseCtx, ask: (input) => - Effect.sync(() => { calls.push(input) }), + Effect.sync(() => { + calls.push(input) + }), } return { ctx, calls } diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index 54e615408b..1f4001a0cb 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -132,13 +132,15 @@ describe("tool.bash", () => { directory: projectRoot, fn: async () => { const bash = await initBash() - const result = await Effect.runPromise(bash.execute( - { - command: "echo test", - description: "Echo test message", - }, - ctx, - )) + const result = await Effect.runPromise( + bash.execute( + { + command: "echo test", + description: "Echo test message", + }, + ctx, + ), + ) expect(result.metadata.exit).toBe(0) expect(result.metadata.output).toContain("test") }, @@ -154,13 +156,15 @@ describe("tool.bash permissions", () => { fn: async () => { const bash = await initBash() const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: "echo hello", - description: "Echo hello", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: "echo hello", + description: "Echo hello", + }, + capture(requests), + ), + ) expect(requests.length).toBe(1) expect(requests[0].permission).toBe("bash") expect(requests[0].patterns).toContain("echo hello") @@ -175,13 +179,15 @@ describe("tool.bash permissions", () => { fn: async () => { const bash = await initBash() const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: "echo foo && echo bar", - description: "Echo twice", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: "echo foo && echo bar", + description: "Echo twice", + }, + capture(requests), + ), + ) expect(requests.length).toBe(1) expect(requests[0].permission).toBe("bash") expect(requests[0].patterns).toContain("echo foo") @@ -199,13 +205,15 @@ describe("tool.bash permissions", () => { fn: async () => { const bash = await initBash() const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: "Write-Host foo; if ($?) { Write-Host bar }", - description: "Check PowerShell conditional", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: "Write-Host foo; if ($?) { Write-Host bar }", + description: "Check PowerShell conditional", + }, + capture(requests), + ), + ) const bashReq = requests.find((r) => r.permission === "bash") expect(bashReq).toBeDefined() expect(bashReq!.patterns).toContain("Write-Host foo") @@ -227,13 +235,15 @@ describe("tool.bash permissions", () => { const file = process.platform === "win32" ? `${process.env.WINDIR!.replaceAll("\\", "/")}/*` : "/etc/*" const want = process.platform === "win32" ? glob(path.join(process.env.WINDIR!, "*")) : "/etc/*" await expect( - Effect.runPromise(bash.execute( - { - command: `cat ${file}`, - description: "Read wildcard path", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: `cat ${file}`, + description: "Read wildcard path", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() @@ -258,13 +268,15 @@ describe("tool.bash permissions", () => { const bash = await initBash() const file = path.join(outerTmp.path, "outside.txt").replaceAll("\\", "/") const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: `echo $(cat "${file}")`, - description: "Read nested bash file", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: `echo $(cat "${file}")`, + description: "Read nested bash file", + }, + capture(requests), + ), + ) const extDirReq = requests.find((r) => r.permission === "external_directory") const bashReq = requests.find((r) => r.permission === "bash") expect(extDirReq).toBeDefined() @@ -290,13 +302,15 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: `Copy-Item -PassThru "${process.env.WINDIR!.replaceAll("\\", "/")}/win.ini" ./out`, - description: "Copy Windows ini", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: `Copy-Item -PassThru "${process.env.WINDIR!.replaceAll("\\", "/")}/win.ini" ./out`, + description: "Copy Windows ini", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() @@ -317,13 +331,15 @@ describe("tool.bash permissions", () => { const bash = await initBash() const requests: Array> = [] const file = `${process.env.WINDIR!.replaceAll("\\", "/")}/win.ini` - await Effect.runPromise(bash.execute( - { - command: `Write-Output $(Get-Content ${file})`, - description: "Read nested PowerShell file", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: `Write-Output $(Get-Content ${file})`, + description: "Read nested PowerShell file", + }, + capture(requests), + ), + ) const extDirReq = requests.find((r) => r.permission === "external_directory") const bashReq = requests.find((r) => r.permission === "bash") expect(extDirReq).toBeDefined() @@ -348,13 +364,15 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: 'Get-Content "C:../outside.txt"', - description: "Read drive-relative file", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: 'Get-Content "C:../outside.txt"', + description: "Read drive-relative file", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) expect(requests[0]?.permission).toBe("external_directory") if (requests[0]?.permission !== "external_directory") return @@ -376,13 +394,15 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: 'Get-Content "$HOME/.ssh/config"', - description: "Read home config", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: 'Get-Content "$HOME/.ssh/config"', + description: "Read home config", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) expect(requests[0]?.permission).toBe("external_directory") if (requests[0]?.permission !== "external_directory") return @@ -405,13 +425,15 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: 'Get-Content "$PWD/../outside.txt"', - description: "Read pwd-relative file", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: 'Get-Content "$PWD/../outside.txt"', + description: "Read pwd-relative file", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) expect(requests[0]?.permission).toBe("external_directory") if (requests[0]?.permission !== "external_directory") return @@ -433,13 +455,15 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: 'Get-Content "$PSHOME/outside.txt"', - description: "Read pshome file", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: 'Get-Content "$PSHOME/outside.txt"', + description: "Read pshome file", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) expect(requests[0]?.permission).toBe("external_directory") if (requests[0]?.permission !== "external_directory") return @@ -466,13 +490,15 @@ describe("tool.bash permissions", () => { const requests: Array> = [] const root = path.parse(process.env.WINDIR!).root.replace(/[\\/]+$/, "") await expect( - Effect.runPromise(bash.execute( - { - command: `Get-Content -Path "${root}$env:${key}\\Windows\\win.ini"`, - description: "Read Windows ini with missing env", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: `Get-Content -Path "${root}$env:${key}\\Windows\\win.ini"`, + description: "Read Windows ini with missing env", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() @@ -496,13 +522,15 @@ describe("tool.bash permissions", () => { fn: async () => { const bash = await initBash() const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: "Get-Content $env:WINDIR/win.ini", - description: "Read Windows ini from env", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: "Get-Content $env:WINDIR/win.ini", + description: "Read Windows ini from env", + }, + capture(requests), + ), + ) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() expect(extDirReq!.patterns).toContain( @@ -525,13 +553,15 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: `Get-Content -Path FileSystem::${process.env.WINDIR!.replaceAll("\\", "/")}/win.ini`, - description: "Read Windows ini from FileSystem provider", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: `Get-Content -Path FileSystem::${process.env.WINDIR!.replaceAll("\\", "/")}/win.ini`, + description: "Read Windows ini from FileSystem provider", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) expect(requests[0]?.permission).toBe("external_directory") if (requests[0]?.permission !== "external_directory") return @@ -555,13 +585,15 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: "Get-Content ${env:WINDIR}/win.ini", - description: "Read Windows ini from braced env", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: "Get-Content ${env:WINDIR}/win.ini", + description: "Read Windows ini from braced env", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) expect(requests[0]?.permission).toBe("external_directory") if (requests[0]?.permission !== "external_directory") return @@ -583,13 +615,15 @@ describe("tool.bash permissions", () => { fn: async () => { const bash = await initBash() const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: "Set-Location C:/Windows", - description: "Change location", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: "Set-Location C:/Windows", + description: "Change location", + }, + capture(requests), + ), + ) const extDirReq = requests.find((r) => r.permission === "external_directory") const bashReq = requests.find((r) => r.permission === "bash") expect(extDirReq).toBeDefined() @@ -612,13 +646,15 @@ describe("tool.bash permissions", () => { fn: async () => { const bash = await initBash() const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: "Write-Output ('a' * 3)", - description: "Write repeated text", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: "Write-Output ('a' * 3)", + description: "Write repeated text", + }, + capture(requests), + ), + ) const bashReq = requests.find((r) => r.permission === "bash") expect(bashReq).toBeDefined() expect(bashReq!.patterns).not.toContain("a * 3") @@ -639,13 +675,15 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: "cd ../", - description: "Change to parent directory", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: "cd ../", + description: "Change to parent directory", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() @@ -662,14 +700,16 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: "echo ok", - workdir: os.tmpdir(), - description: "Echo from temp dir", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: "echo ok", + workdir: os.tmpdir(), + description: "Echo from temp dir", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeDefined() @@ -692,14 +732,16 @@ describe("tool.bash permissions", () => { for (const dir of forms(outerTmp.path)) { const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { - command: "echo ok", - workdir: dir, - description: "Echo from external dir", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: "echo ok", + workdir: dir, + description: "Echo from external dir", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) const extDirReq = requests.find((r) => r.permission === "external_directory") @@ -725,14 +767,16 @@ describe("tool.bash permissions", () => { const requests: Array> = [] const want = glob(path.join(os.tmpdir(), "*")) await expect( - Effect.runPromise(bash.execute( - { - command: "echo ok", - workdir: "/tmp", - description: "Echo from Git Bash tmp", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: "echo ok", + workdir: "/tmp", + description: "Echo from Git Bash tmp", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) expect(requests[0]).toMatchObject({ permission: "external_directory", @@ -755,13 +799,15 @@ describe("tool.bash permissions", () => { const requests: Array> = [] const want = glob(path.join(os.tmpdir(), "*")) await expect( - Effect.runPromise(bash.execute( - { - command: "cat /tmp/opencode-does-not-exist", - description: "Read Git Bash tmp file", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: "cat /tmp/opencode-does-not-exist", + description: "Read Git Bash tmp file", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) expect(requests[0]).toMatchObject({ permission: "external_directory", @@ -790,13 +836,15 @@ describe("tool.bash permissions", () => { const requests: Array> = [] const filepath = path.join(outerTmp.path, "outside.txt") await expect( - Effect.runPromise(bash.execute( - { - command: `cat ${filepath}`, - description: "Read external file", - }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { + command: `cat ${filepath}`, + description: "Read external file", + }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) const extDirReq = requests.find((r) => r.permission === "external_directory") const expected = glob(path.join(outerTmp.path, "*")) @@ -818,13 +866,15 @@ describe("tool.bash permissions", () => { fn: async () => { const bash = await initBash() const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: `rm -rf ${path.join(tmp.path, "nested")}`, - description: "Remove nested dir", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: `rm -rf ${path.join(tmp.path, "nested")}`, + description: "Remove nested dir", + }, + capture(requests), + ), + ) const extDirReq = requests.find((r) => r.permission === "external_directory") expect(extDirReq).toBeUndefined() }, @@ -838,13 +888,15 @@ describe("tool.bash permissions", () => { fn: async () => { const bash = await initBash() const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: "git log --oneline -5", - description: "Git log", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: "git log --oneline -5", + description: "Git log", + }, + capture(requests), + ), + ) expect(requests.length).toBe(1) expect(requests[0].always.length).toBeGreaterThan(0) expect(requests[0].always.some((item) => item.endsWith("*"))).toBe(true) @@ -859,13 +911,15 @@ describe("tool.bash permissions", () => { fn: async () => { const bash = await initBash() const requests: Array> = [] - await Effect.runPromise(bash.execute( - { - command: "cd .", - description: "Stay in current directory", - }, - capture(requests), - )) + await Effect.runPromise( + bash.execute( + { + command: "cd .", + description: "Stay in current directory", + }, + capture(requests), + ), + ) const bashReq = requests.find((r) => r.permission === "bash") expect(bashReq).toBeUndefined() }, @@ -881,10 +935,12 @@ describe("tool.bash permissions", () => { const err = new Error("stop after permission") const requests: Array> = [] await expect( - Effect.runPromise(bash.execute( - { command: "echo test > output.txt", description: "Redirect test output" }, - capture(requests, err), - )), + Effect.runPromise( + bash.execute( + { command: "echo test > output.txt", description: "Redirect test output" }, + capture(requests, err), + ), + ), ).rejects.toThrow(err.message) const bashReq = requests.find((r) => r.permission === "bash") expect(bashReq).toBeDefined() @@ -917,23 +973,25 @@ describe("tool.bash abort", () => { const bash = await initBash() const controller = new AbortController() const collected: string[] = [] - const res = await Effect.runPromise(bash.execute( - { - command: `echo before && sleep 30`, - description: "Long running command", - }, - { - ...ctx, - abort: controller.signal, - metadata: (input) => { - const output = (input.metadata as { output?: string })?.output - if (output && output.includes("before") && !controller.signal.aborted) { - collected.push(output) - controller.abort() - } + const res = await Effect.runPromise( + bash.execute( + { + command: `echo before && sleep 30`, + description: "Long running command", }, - }, - )) + { + ...ctx, + abort: controller.signal, + metadata: (input) => { + const output = (input.metadata as { output?: string })?.output + if (output && output.includes("before") && !controller.signal.aborted) { + collected.push(output) + controller.abort() + } + }, + }, + ), + ) expect(res.output).toContain("before") expect(res.output).toContain("User aborted the command") expect(collected.length).toBeGreaterThan(0) @@ -946,14 +1004,16 @@ describe("tool.bash abort", () => { directory: projectRoot, fn: async () => { const bash = await initBash() - const result = await Effect.runPromise(bash.execute( - { - command: `echo started && sleep 60`, - description: "Timeout test", - timeout: 500, - }, - ctx, - )) + const result = await Effect.runPromise( + bash.execute( + { + command: `echo started && sleep 60`, + description: "Timeout test", + timeout: 500, + }, + ctx, + ), + ) expect(result.output).toContain("started") expect(result.output).toContain("bash tool terminated command after exceeding timeout") }, @@ -965,13 +1025,15 @@ describe("tool.bash abort", () => { directory: projectRoot, fn: async () => { const bash = await initBash() - const result = await Effect.runPromise(bash.execute( - { - command: `echo stdout_msg && echo stderr_msg >&2`, - description: "Stderr test", - }, - ctx, - )) + const result = await Effect.runPromise( + bash.execute( + { + command: `echo stdout_msg && echo stderr_msg >&2`, + description: "Stderr test", + }, + ctx, + ), + ) expect(result.output).toContain("stdout_msg") expect(result.output).toContain("stderr_msg") expect(result.metadata.exit).toBe(0) @@ -984,13 +1046,15 @@ describe("tool.bash abort", () => { directory: projectRoot, fn: async () => { const bash = await initBash() - const result = await Effect.runPromise(bash.execute( - { - command: `exit 42`, - description: "Non-zero exit", - }, - ctx, - )) + const result = await Effect.runPromise( + bash.execute( + { + command: `exit 42`, + description: "Non-zero exit", + }, + ctx, + ), + ) expect(result.metadata.exit).toBe(42) }, }) @@ -1002,19 +1066,21 @@ describe("tool.bash abort", () => { fn: async () => { const bash = await initBash() const updates: string[] = [] - const result = await Effect.runPromise(bash.execute( - { - command: `echo first && sleep 0.1 && echo second`, - description: "Streaming test", - }, - { - ...ctx, - metadata: (input) => { - const output = (input.metadata as { output?: string })?.output - if (output) updates.push(output) + const result = await Effect.runPromise( + bash.execute( + { + command: `echo first && sleep 0.1 && echo second`, + description: "Streaming test", }, - }, - )) + { + ...ctx, + metadata: (input) => { + const output = (input.metadata as { output?: string })?.output + if (output) updates.push(output) + }, + }, + ), + ) expect(result.output).toContain("first") expect(result.output).toContain("second") expect(updates.length).toBeGreaterThan(1) @@ -1030,13 +1096,15 @@ describe("tool.bash truncation", () => { fn: async () => { const bash = await initBash() const lineCount = Truncate.MAX_LINES + 500 - const result = await Effect.runPromise(bash.execute( - { - command: fill("lines", lineCount), - description: "Generate lines exceeding limit", - }, - ctx, - )) + const result = await Effect.runPromise( + bash.execute( + { + command: fill("lines", lineCount), + description: "Generate lines exceeding limit", + }, + ctx, + ), + ) mustTruncate(result) expect(result.output).toContain("truncated") expect(result.output).toContain("The tool call succeeded but the output was truncated") @@ -1050,13 +1118,15 @@ describe("tool.bash truncation", () => { fn: async () => { const bash = await initBash() const byteCount = Truncate.MAX_BYTES + 10000 - const result = await Effect.runPromise(bash.execute( - { - command: fill("bytes", byteCount), - description: "Generate bytes exceeding limit", - }, - ctx, - )) + const result = await Effect.runPromise( + bash.execute( + { + command: fill("bytes", byteCount), + description: "Generate bytes exceeding limit", + }, + ctx, + ), + ) mustTruncate(result) expect(result.output).toContain("truncated") expect(result.output).toContain("The tool call succeeded but the output was truncated") @@ -1069,13 +1139,15 @@ describe("tool.bash truncation", () => { directory: projectRoot, fn: async () => { const bash = await initBash() - const result = await Effect.runPromise(bash.execute( - { - command: "echo hello", - description: "Echo hello", - }, - ctx, - )) + const result = await Effect.runPromise( + bash.execute( + { + command: "echo hello", + description: "Echo hello", + }, + ctx, + ), + ) expect((result.metadata as { truncated?: boolean }).truncated).toBe(false) expect(result.output).toContain("hello") }, @@ -1088,13 +1160,15 @@ describe("tool.bash truncation", () => { fn: async () => { const bash = await initBash() const lineCount = Truncate.MAX_LINES + 100 - const result = await Effect.runPromise(bash.execute( - { - command: fill("lines", lineCount), - description: "Generate lines for file check", - }, - ctx, - )) + const result = await Effect.runPromise( + bash.execute( + { + command: fill("lines", lineCount), + description: "Generate lines for file check", + }, + ctx, + ), + ) mustTruncate(result) const filepath = (result.metadata as { outputPath?: string }).outputPath diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 21dc57e368..df58f6aa3f 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -65,14 +65,16 @@ describe("tool.edit", () => { directory: tmp.path, fn: async () => { const edit = await resolve() - const result = await Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "", - newString: "new content", - }, - ctx, - )) + const result = await Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "", + newString: "new content", + }, + ctx, + ), + ) expect(result.metadata.diff).toContain("new content") @@ -90,14 +92,16 @@ describe("tool.edit", () => { directory: tmp.path, fn: async () => { const edit = await resolve() - await Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "", - newString: "nested file", - }, - ctx, - )) + await Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "", + newString: "nested file", + }, + ctx, + ), + ) const content = await fs.readFile(filepath, "utf-8") expect(content).toBe("nested file") @@ -118,14 +122,16 @@ describe("tool.edit", () => { const unsubUpdated = await subscribeBus(FileWatcher.Event.Updated, () => events.push("updated")) const edit = await resolve() - await Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "", - newString: "content", - }, - ctx, - )) + await Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "", + newString: "content", + }, + ctx, + ), + ) expect(events).toContain("updated") unsubUpdated() @@ -146,14 +152,16 @@ describe("tool.edit", () => { await readFileTime(ctx.sessionID, filepath) const edit = await resolve() - const result = await Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "old content", - newString: "new content", - }, - ctx, - )) + const result = await Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "old content", + newString: "new content", + }, + ctx, + ), + ) expect(result.output).toContain("Edit applied successfully") @@ -174,14 +182,16 @@ describe("tool.edit", () => { const edit = await resolve() await expect( - Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "old", - newString: "new", - }, - ctx, - )), + Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "old", + newString: "new", + }, + ctx, + ), + ), ).rejects.toThrow("not found") }, }) @@ -197,14 +207,16 @@ describe("tool.edit", () => { fn: async () => { const edit = await resolve() await expect( - Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "same", - newString: "same", - }, - ctx, - )), + Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "same", + newString: "same", + }, + ctx, + ), + ), ).rejects.toThrow("identical") }, }) @@ -222,14 +234,16 @@ describe("tool.edit", () => { const edit = await resolve() await expect( - Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "not in file", - newString: "replacement", - }, - ctx, - )), + Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "not in file", + newString: "replacement", + }, + ctx, + ), + ), ).rejects.toThrow() }, }) @@ -245,14 +259,16 @@ describe("tool.edit", () => { fn: async () => { const edit = await resolve() await expect( - Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "content", - newString: "modified", - }, - ctx, - )), + Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "content", + newString: "modified", + }, + ctx, + ), + ), ).rejects.toThrow("You must read file") }, }) @@ -277,14 +293,16 @@ describe("tool.edit", () => { // Try to edit with the new content const edit = await resolve() await expect( - Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "modified externally", - newString: "edited", - }, - ctx, - )), + Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "modified externally", + newString: "edited", + }, + ctx, + ), + ), ).rejects.toThrow("modified since it was last read") }, }) @@ -301,15 +319,17 @@ describe("tool.edit", () => { await readFileTime(ctx.sessionID, filepath) const edit = await resolve() - await Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "foo", - newString: "qux", - replaceAll: true, - }, - ctx, - )) + await Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "foo", + newString: "qux", + replaceAll: true, + }, + ctx, + ), + ) const content = await fs.readFile(filepath, "utf-8") expect(content).toBe("qux bar qux baz qux") @@ -333,14 +353,16 @@ describe("tool.edit", () => { const unsubUpdated = await subscribeBus(FileWatcher.Event.Updated, () => events.push("updated")) const edit = await resolve() - await Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "original", - newString: "modified", - }, - ctx, - )) + await Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "original", + newString: "modified", + }, + ctx, + ), + ) expect(events).toContain("updated") unsubUpdated() @@ -361,14 +383,16 @@ describe("tool.edit", () => { await readFileTime(ctx.sessionID, filepath) const edit = await resolve() - await Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "line2", - newString: "new line 2\nextra line", - }, - ctx, - )) + await Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "line2", + newString: "new line 2\nextra line", + }, + ctx, + ), + ) const content = await fs.readFile(filepath, "utf-8") expect(content).toBe("line1\nnew line 2\nextra line\nline3") @@ -387,14 +411,16 @@ describe("tool.edit", () => { await readFileTime(ctx.sessionID, filepath) const edit = await resolve() - await Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "old", - newString: "new", - }, - ctx, - )) + await Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "old", + newString: "new", + }, + ctx, + ), + ) const content = await fs.readFile(filepath, "utf-8") expect(content).toBe("line1\r\nnew\r\nline3") @@ -412,14 +438,16 @@ describe("tool.edit", () => { fn: async () => { const edit = await resolve() await expect( - Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "", - newString: "", - }, - ctx, - )), + Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "", + newString: "", + }, + ctx, + ), + ), ).rejects.toThrow("identical") }, }) @@ -437,14 +465,16 @@ describe("tool.edit", () => { const edit = await resolve() await expect( - Effect.runPromise(edit.execute( - { - filePath: dirpath, - oldString: "old", - newString: "new", - }, - ctx, - )), + Effect.runPromise( + edit.execute( + { + filePath: dirpath, + oldString: "old", + newString: "new", + }, + ctx, + ), + ), ).rejects.toThrow("directory") }, }) @@ -461,14 +491,16 @@ describe("tool.edit", () => { await readFileTime(ctx.sessionID, filepath) 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 = await Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "line2", + newString: "new line a\nnew line b", + }, + ctx, + ), + ) expect(result.metadata.filediff).toBeDefined() expect(result.metadata.filediff.file).toBe(filepath) @@ -530,15 +562,17 @@ describe("tool.edit", () => { const edit = await resolve() const filePath = path.join(tmp.path, "test.txt") await readFileTime(ctx.sessionID, filePath) - await Effect.runPromise(edit.execute( - { - filePath, - oldString: input.oldString, - newString: input.newString, - replaceAll: input.replaceAll, - }, - ctx, - )) + await Effect.runPromise( + edit.execute( + { + filePath, + oldString: input.oldString, + newString: input.newString, + replaceAll: input.replaceAll, + }, + ctx, + ), + ) return await Bun.file(filePath).text() }, }) @@ -675,26 +709,30 @@ describe("tool.edit", () => { const edit = await resolve() // Two concurrent edits - const promise1 = Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "0", - newString: "1", - }, - ctx, - )) + const promise1 = Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "0", + newString: "1", + }, + ctx, + ), + ) // Need to read again since FileTime tracks per-session await readFileTime(ctx.sessionID, filepath) - const promise2 = Effect.runPromise(edit.execute( - { - filePath: filepath, - oldString: "0", - newString: "2", - }, - ctx, - )) + const promise2 = Effect.runPromise( + edit.execute( + { + filePath: filepath, + oldString: "0", + newString: "2", + }, + ctx, + ), + ) // Both should complete without error (though one might fail due to content mismatch) const results = await Promise.allSettled([promise1, promise2]) diff --git a/packages/opencode/test/tool/external-directory.test.ts b/packages/opencode/test/tool/external-directory.test.ts index dd739b5f68..79b58c1368 100644 --- a/packages/opencode/test/tool/external-directory.test.ts +++ b/packages/opencode/test/tool/external-directory.test.ts @@ -26,7 +26,10 @@ function makeCtx() { const requests: Array> = [] const ctx: Tool.Context = { ...baseCtx, - ask: (req) => Effect.sync(() => { requests.push(req) }), + ask: (req) => + Effect.sync(() => { + requests.push(req) + }), } return { requests, ctx } } diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index c35c5c08f2..f907b626ec 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -32,14 +32,16 @@ describe("tool.grep", () => { directory: projectRoot, fn: async () => { const grep = await initGrep() - const result = await Effect.runPromise(grep.execute( - { - pattern: "export", - path: path.join(projectRoot, "src/tool"), - include: "*.ts", - }, - ctx, - )) + const result = await Effect.runPromise( + grep.execute( + { + pattern: "export", + path: path.join(projectRoot, "src/tool"), + include: "*.ts", + }, + ctx, + ), + ) expect(result.metadata.matches).toBeGreaterThan(0) expect(result.output).toContain("Found") }, @@ -56,13 +58,15 @@ describe("tool.grep", () => { directory: tmp.path, fn: async () => { const grep = await initGrep() - const result = await Effect.runPromise(grep.execute( - { - pattern: "xyznonexistentpatternxyz123", - path: tmp.path, - }, - ctx, - )) + const result = await Effect.runPromise( + grep.execute( + { + pattern: "xyznonexistentpatternxyz123", + path: tmp.path, + }, + ctx, + ), + ) expect(result.metadata.matches).toBe(0) expect(result.output).toBe("No files found") }, @@ -81,13 +85,15 @@ describe("tool.grep", () => { directory: tmp.path, fn: async () => { const grep = await initGrep() - const result = await Effect.runPromise(grep.execute( - { - pattern: "line", - path: tmp.path, - }, - ctx, - )) + const result = await Effect.runPromise( + grep.execute( + { + pattern: "line", + path: tmp.path, + }, + ctx, + ), + ) expect(result.metadata.matches).toBeGreaterThan(0) }, }) diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index 888cc4225f..a48cd4755a 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -96,7 +96,9 @@ const asks = () => { next: { ...ctx, ask: (req: Omit) => - Effect.sync(() => { items.push(req) }), + Effect.sync(() => { + items.push(req) + }), }, } } diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index bfde5835f0..ebd62dbcf9 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -157,7 +157,9 @@ Use this skill. const ctx: Tool.Context = { ...baseCtx, ask: (req) => - Effect.sync(() => { requests.push(req) }), + Effect.sync(() => { + requests.push(req) + }), } const result = await runtime.runPromise(tool.execute({ name: "tool-skill" }, ctx)) diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index f7288ad610..478ff96628 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -195,23 +195,23 @@ describe("tool.task", () => { const promptOps = stubOps({ text: "resumed", onPrompt: (input) => (seen = input) }) const result = yield* def.execute( - { - description: "inspect bug", - prompt: "look into the cache key path", - subagent_type: "general", - task_id: child.id, - }, - { - sessionID: chat.id, - messageID: assistant.id, - agent: "build", - abort: new AbortController().signal, - extra: { promptOps }, - messages: [], - metadata() {}, - ask: () => Effect.void, - }, - ) + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + task_id: child.id, + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata() {}, + ask: () => Effect.void, + }, + ) const kids = yield* sessions.children(chat.id) expect(kids).toHaveLength(1) @@ -234,23 +234,25 @@ describe("tool.task", () => { const exec = (extra?: Record) => def.execute( - { - description: "inspect bug", - prompt: "look into the cache key path", - subagent_type: "general", - }, - { - sessionID: chat.id, - messageID: assistant.id, - agent: "build", - abort: new AbortController().signal, - extra: { promptOps, ...extra }, - messages: [], - metadata() {}, - ask: (input) => - Effect.sync(() => { calls.push(input) }), - }, - ) + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps, ...extra }, + messages: [], + metadata() {}, + ask: (input) => + Effect.sync(() => { + calls.push(input) + }), + }, + ) yield* exec() yield* exec({ bypassAgentCheck: true }) @@ -280,23 +282,23 @@ describe("tool.task", () => { const promptOps = stubOps({ text: "created", onPrompt: (input) => (seen = input) }) const result = yield* def.execute( - { - description: "inspect bug", - prompt: "look into the cache key path", - subagent_type: "general", - task_id: "ses_missing", - }, - { - sessionID: chat.id, - messageID: assistant.id, - agent: "build", - abort: new AbortController().signal, - extra: { promptOps }, - messages: [], - metadata() {}, - ask: () => Effect.void, - }, - ) + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + task_id: "ses_missing", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata() {}, + ask: () => Effect.void, + }, + ) const kids = yield* sessions.children(chat.id) expect(kids).toHaveLength(1) @@ -320,22 +322,22 @@ describe("tool.task", () => { const promptOps = stubOps({ onPrompt: (input) => (seen = input) }) const result = yield* def.execute( - { - description: "inspect bug", - prompt: "look into the cache key path", - subagent_type: "reviewer", - }, - { - sessionID: chat.id, - messageID: assistant.id, - agent: "build", - abort: new AbortController().signal, - extra: { promptOps }, - messages: [], - metadata() {}, - ask: () => Effect.void, - }, - ) + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "reviewer", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + extra: { promptOps }, + messages: [], + metadata() {}, + ask: () => Effect.void, + }, + ) const child = yield* sessions.get(result.metadata.sessionId) expect(child.parentID).toBe(chat.id) diff --git a/packages/opencode/test/tool/tool-define.test.ts b/packages/opencode/test/tool/tool-define.test.ts index 4b44e17b09..1f8f43b2d1 100644 --- a/packages/opencode/test/tool/tool-define.test.ts +++ b/packages/opencode/test/tool/tool-define.test.ts @@ -32,7 +32,10 @@ describe("Tool.define", () => { test("function-defined tool returns fresh objects and is unaffected", async () => { const info = await Effect.runPromise( - Tool.define("test-fn-tool", Effect.succeed(() => Promise.resolve(makeTool("test")))), + Tool.define( + "test-fn-tool", + Effect.succeed(() => Promise.resolve(makeTool("test"))), + ), ) const first = await info.init() diff --git a/packages/opencode/test/tool/webfetch.test.ts b/packages/opencode/test/tool/webfetch.test.ts index 8e9f96808c..d601f0debd 100644 --- a/packages/opencode/test/tool/webfetch.test.ts +++ b/packages/opencode/test/tool/webfetch.test.ts @@ -42,10 +42,9 @@ describe("tool.webfetch", () => { directory: projectRoot, fn: async () => { const webfetch = await initTool() - const result = await Effect.runPromise(webfetch.execute( - { url: new URL("/image.png", url).toString(), format: "markdown" }, - ctx, - )) + const result = await Effect.runPromise( + webfetch.execute({ url: new URL("/image.png", url).toString(), format: "markdown" }, ctx), + ) expect(result.output).toBe("Image fetched successfully") expect(result.attachments).toBeDefined() expect(result.attachments?.length).toBe(1) @@ -74,7 +73,9 @@ describe("tool.webfetch", () => { directory: projectRoot, fn: async () => { const webfetch = await initTool() - const result = await Effect.runPromise(webfetch.execute({ url: new URL("/image.svg", url).toString(), format: "html" }, ctx)) + const result = await Effect.runPromise( + webfetch.execute({ url: new URL("/image.svg", url).toString(), format: "html" }, ctx), + ) expect(result.output).toContain(" { directory: projectRoot, fn: async () => { const webfetch = await initTool() - const result = await Effect.runPromise(webfetch.execute({ url: new URL("/file.txt", url).toString(), format: "text" }, ctx)) + const result = await Effect.runPromise( + webfetch.execute({ url: new URL("/file.txt", url).toString(), format: "text" }, ctx), + ) expect(result.output).toBe("hello from webfetch") expect(result.attachments).toBeUndefined() }, diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 57d2cd6a76..aac55f1e70 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -31,7 +31,14 @@ afterEach(async () => { }) const it = testEffect( - Layer.mergeAll(LSP.defaultLayer, AppFileSystem.defaultLayer, FileTime.defaultLayer, Bus.layer, Format.defaultLayer, CrossSpawnSpawner.defaultLayer), + Layer.mergeAll( + LSP.defaultLayer, + AppFileSystem.defaultLayer, + FileTime.defaultLayer, + Bus.layer, + Format.defaultLayer, + CrossSpawnSpawner.defaultLayer, + ), ) const init = Effect.fn("WriteToolTest.init")(function* () {