mirror of
https://fastgit.cc/https://github.com/anomalyco/opencode
synced 2026-04-21 13:21:17 +08:00
chore: generate
This commit is contained in:
@@ -106,8 +106,7 @@ export namespace SessionPrompt {
|
||||
const run = {
|
||||
promise: <A, E>(effect: Effect.Effect<A, E>) =>
|
||||
Effect.runPromise(effect.pipe(Effect.provide(EffectLogger.layer))),
|
||||
fork: <A, E>(effect: Effect.Effect<A, E>) =>
|
||||
Effect.runFork(effect.pipe(Effect.provide(EffectLogger.layer))),
|
||||
fork: <A, E>(effect: Effect.Effect<A, E>) => 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) => ({
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -26,7 +26,10 @@ function makeCtx() {
|
||||
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
|
||||
const ctx: Tool.Context = {
|
||||
...baseCtx,
|
||||
ask: (req) => Effect.sync(() => { requests.push(req) }),
|
||||
ask: (req) =>
|
||||
Effect.sync(() => {
|
||||
requests.push(req)
|
||||
}),
|
||||
}
|
||||
return { requests, ctx }
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
},
|
||||
})
|
||||
|
||||
@@ -96,7 +96,9 @@ const asks = () => {
|
||||
next: {
|
||||
...ctx,
|
||||
ask: (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) =>
|
||||
Effect.sync(() => { items.push(req) }),
|
||||
Effect.sync(() => {
|
||||
items.push(req)
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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<string, any>) =>
|
||||
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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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("<svg")
|
||||
expect(result.attachments).toBeUndefined()
|
||||
},
|
||||
@@ -95,7 +96,9 @@ describe("tool.webfetch", () => {
|
||||
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()
|
||||
},
|
||||
|
||||
@@ -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* () {
|
||||
|
||||
Reference in New Issue
Block a user