f31006b1cd
Replaces legacy Permission.respond with PermissionNext.reply for better async handling and updates server endpoints to use the new permission system. Improves error handling in session processor to work with both old and new permission rejection types.
203 lines
6.2 KiB
TypeScript
203 lines
6.2 KiB
TypeScript
import { describe, expect, test } from "bun:test"
|
|
import path from "path"
|
|
import { BashTool } from "../../src/tool/bash"
|
|
import { Instance } from "../../src/project/instance"
|
|
import { tmpdir } from "../fixture/fixture"
|
|
import type { PermissionNext } from "../../src/permission/next"
|
|
|
|
const ctx = {
|
|
sessionID: "test",
|
|
messageID: "",
|
|
callID: "",
|
|
agent: "build",
|
|
abort: AbortSignal.any([]),
|
|
metadata: () => {},
|
|
ask: async () => {},
|
|
}
|
|
|
|
const projectRoot = path.join(__dirname, "../..")
|
|
|
|
describe("tool.bash", () => {
|
|
test("basic", async () => {
|
|
await Instance.provide({
|
|
directory: projectRoot,
|
|
fn: async () => {
|
|
const bash = await BashTool.init()
|
|
const result = await bash.execute(
|
|
{
|
|
command: "echo 'test'",
|
|
description: "Echo test message",
|
|
},
|
|
ctx,
|
|
)
|
|
expect(result.metadata.exit).toBe(0)
|
|
expect(result.metadata.output).toContain("test")
|
|
},
|
|
})
|
|
})
|
|
})
|
|
|
|
describe("tool.bash permissions", () => {
|
|
test("asks for bash permission with correct pattern", async () => {
|
|
await using tmp = await tmpdir({ git: true })
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const bash = await BashTool.init()
|
|
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
const testCtx = {
|
|
...ctx,
|
|
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
requests.push(req)
|
|
},
|
|
}
|
|
await bash.execute(
|
|
{
|
|
command: "echo hello",
|
|
description: "Echo hello",
|
|
},
|
|
testCtx,
|
|
)
|
|
expect(requests.length).toBe(1)
|
|
expect(requests[0].permission).toBe("bash")
|
|
expect(requests[0].patterns).toContain("echo hello")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("asks for bash permission with multiple commands", async () => {
|
|
await using tmp = await tmpdir({ git: true })
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const bash = await BashTool.init()
|
|
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
const testCtx = {
|
|
...ctx,
|
|
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
requests.push(req)
|
|
},
|
|
}
|
|
await bash.execute(
|
|
{
|
|
command: "echo foo && echo bar",
|
|
description: "Echo twice",
|
|
},
|
|
testCtx,
|
|
)
|
|
expect(requests.length).toBe(1)
|
|
expect(requests[0].permission).toBe("bash")
|
|
expect(requests[0].patterns).toContain("echo foo")
|
|
expect(requests[0].patterns).toContain("echo bar")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("asks for external_directory permission when cd to parent", async () => {
|
|
await using tmp = await tmpdir({ git: true })
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const bash = await BashTool.init()
|
|
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
const testCtx = {
|
|
...ctx,
|
|
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
requests.push(req)
|
|
},
|
|
}
|
|
await bash.execute(
|
|
{
|
|
command: "cd ../",
|
|
description: "Change to parent directory",
|
|
},
|
|
testCtx,
|
|
)
|
|
const extDirReq = requests.find((r) => r.permission === "external_directory")
|
|
expect(extDirReq).toBeDefined()
|
|
},
|
|
})
|
|
})
|
|
|
|
test("asks for external_directory permission when workdir is outside project", async () => {
|
|
await using tmp = await tmpdir({ git: true })
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const bash = await BashTool.init()
|
|
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
const testCtx = {
|
|
...ctx,
|
|
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
requests.push(req)
|
|
},
|
|
}
|
|
await bash.execute(
|
|
{
|
|
command: "ls",
|
|
workdir: "/tmp",
|
|
description: "List /tmp",
|
|
},
|
|
testCtx,
|
|
)
|
|
const extDirReq = requests.find((r) => r.permission === "external_directory")
|
|
expect(extDirReq).toBeDefined()
|
|
expect(extDirReq!.patterns).toContain("/tmp")
|
|
},
|
|
})
|
|
})
|
|
|
|
test("includes always patterns for auto-approval", async () => {
|
|
await using tmp = await tmpdir({ git: true })
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const bash = await BashTool.init()
|
|
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
const testCtx = {
|
|
...ctx,
|
|
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
requests.push(req)
|
|
},
|
|
}
|
|
await bash.execute(
|
|
{
|
|
command: "git log --oneline -5",
|
|
description: "Git log",
|
|
},
|
|
testCtx,
|
|
)
|
|
expect(requests.length).toBe(1)
|
|
expect(requests[0].always.length).toBeGreaterThan(0)
|
|
expect(requests[0].always.some((p) => p.endsWith("*"))).toBe(true)
|
|
},
|
|
})
|
|
})
|
|
|
|
test("does not ask for bash permission when command is cd only", async () => {
|
|
await using tmp = await tmpdir({ git: true })
|
|
await Instance.provide({
|
|
directory: tmp.path,
|
|
fn: async () => {
|
|
const bash = await BashTool.init()
|
|
const requests: Array<Omit<PermissionNext.Request, "id" | "sessionID" | "tool">> = []
|
|
const testCtx = {
|
|
...ctx,
|
|
ask: async (req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) => {
|
|
requests.push(req)
|
|
},
|
|
}
|
|
await bash.execute(
|
|
{
|
|
command: "cd .",
|
|
description: "Stay in current directory",
|
|
},
|
|
testCtx,
|
|
)
|
|
const bashReq = requests.find((r) => r.permission === "bash")
|
|
expect(bashReq).toBeUndefined()
|
|
},
|
|
})
|
|
})
|
|
})
|