test(effect): simplify read tool harness

Reduce helper indirection in the read tool tests by adding a scope-local run helper, reusing a shared permission capture helper, and keeping env-permission assertions inside a single instance scope.
pull/21016/head
Kit Langton 2026-04-04 11:27:31 -04:00
parent 6a56bd5e79
commit 98384cd860
1 changed files with 51 additions and 64 deletions

View File

@ -49,17 +49,20 @@ const init = Effect.fn("ReadToolTest.init")(function* () {
return yield* Effect.promise(() => info.init())
})
const run = Effect.fn("ReadToolTest.run")(function* (
args: Tool.InferParameters<typeof ReadTool>,
next: Tool.Context = ctx,
) {
const tool = yield* init()
return yield* Effect.promise(() => tool.execute(args, next))
})
const exec = Effect.fn("ReadToolTest.exec")(function* (
dir: string,
args: Tool.InferParameters<typeof ReadTool>,
next: Tool.Context = ctx,
) {
return yield* provideInstance(dir)(
Effect.gen(function* () {
const tool = yield* init()
return yield* Effect.promise(() => tool.execute(args, next))
}),
)
return yield* provideInstance(dir)(run(args, next))
})
const fail = Effect.fn("ReadToolTest.fail")(function* (
@ -86,6 +89,18 @@ const load = Effect.fn("ReadToolTest.load")(function* (p: string) {
const fs = yield* AppFileSystem.Service
return yield* fs.readFileString(p)
})
const asks = () => {
const items: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
return {
items,
next: {
...ctx,
ask: async (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) => {
items.push(req)
},
},
}
}
describe("tool.read external_directory permission", () => {
it.live("allows reading absolute path inside project directory", () =>
@ -114,16 +129,10 @@ describe("tool.read external_directory permission", () => {
const dir = yield* tmpdirScoped({ git: true })
yield* put(path.join(outer, "secret.txt"), "secret data")
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const next = {
...ctx,
ask: async (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) => {
requests.push(req)
},
}
const { items, next } = asks()
yield* exec(dir, { filePath: path.join(outer, "secret.txt") }, next)
const ext = requests.find((item) => item.permission === "external_directory")
const ext = items.find((item) => item.permission === "external_directory")
expect(ext).toBeDefined()
expect(ext!.patterns).toContain(glob(path.join(outer, "*")))
}),
@ -135,13 +144,7 @@ describe("tool.read external_directory permission", () => {
const dir = yield* tmpdirScoped({ git: true })
yield* put(path.join(dir, "test.txt"), "hello world")
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const next = {
...ctx,
ask: async (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) => {
requests.push(req)
},
}
const { items, next } = asks()
const target = path.join(dir, "test.txt")
const alt = target
.replace(/^[A-Za-z]:/, "")
@ -149,7 +152,7 @@ describe("tool.read external_directory permission", () => {
.toLowerCase()
yield* exec(dir, { filePath: alt }, next)
const read = requests.find((item) => item.permission === "read")
const read = items.find((item) => item.permission === "read")
expect(read).toBeDefined()
expect(read!.patterns).toEqual([full(target)])
}),
@ -162,16 +165,10 @@ describe("tool.read external_directory permission", () => {
const dir = yield* tmpdirScoped({ git: true })
yield* put(path.join(outer, "external", "a.txt"), "a")
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const next = {
...ctx,
ask: async (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) => {
requests.push(req)
},
}
const { items, next } = asks()
yield* exec(dir, { filePath: path.join(outer, "external") }, next)
const ext = requests.find((item) => item.permission === "external_directory")
const ext = items.find((item) => item.permission === "external_directory")
expect(ext).toBeDefined()
expect(ext!.patterns).toContain(glob(path.join(outer, "external", "*")))
}),
@ -181,16 +178,10 @@ describe("tool.read external_directory permission", () => {
Effect.gen(function* () {
const dir = yield* tmpdirScoped({ git: true })
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const next = {
...ctx,
ask: async (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) => {
requests.push(req)
},
}
const { items, next } = asks()
yield* fail(dir, { filePath: "../outside.txt" }, next)
const ext = requests.find((item) => item.permission === "external_directory")
const ext = items.find((item) => item.permission === "external_directory")
expect(ext).toBeDefined()
}),
)
@ -200,16 +191,10 @@ describe("tool.read external_directory permission", () => {
const dir = yield* tmpdirScoped({ git: true })
yield* put(path.join(dir, "internal.txt"), "internal content")
const requests: Array<Omit<Permission.Request, "id" | "sessionID" | "tool">> = []
const next = {
...ctx,
ask: async (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) => {
requests.push(req)
},
}
const { items, next } = asks()
yield* exec(dir, { filePath: path.join(dir, "internal.txt") }, next)
const ext = requests.find((item) => item.permission === "external_directory")
const ext = items.find((item) => item.permission === "external_directory")
expect(ext).toBeUndefined()
}),
)
@ -234,29 +219,31 @@ describe("tool.read env file permissions", () => {
const dir = yield* tmpdirScoped()
yield* put(path.join(dir, filename), "content")
const info = yield* provideInstance(dir)(
const asked = yield* provideInstance(dir)(
Effect.gen(function* () {
const agent = yield* Agent.Service
return yield* agent.get(agentName)
const info = yield* agent.get(agentName)
let asked = false
const next = {
...ctx,
ask: async (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) => {
for (const pattern of req.patterns) {
const rule = Permission.evaluate(req.permission, pattern, info.permission)
if (rule.action === "ask" && req.permission === "read") {
asked = true
}
if (rule.action === "deny") {
throw new Permission.DeniedError({ ruleset: info.permission })
}
}
},
}
yield* run({ filePath: path.join(dir, filename) }, next)
return asked
}),
)
let asked = false
const next = {
...ctx,
ask: async (req: Omit<Permission.Request, "id" | "sessionID" | "tool">) => {
for (const pattern of req.patterns) {
const rule = Permission.evaluate(req.permission, pattern, info.permission)
if (rule.action === "ask" && req.permission === "read") {
asked = true
}
if (rule.action === "deny") {
throw new Permission.DeniedError({ ruleset: info.permission })
}
}
},
}
yield* exec(dir, { filePath: path.join(dir, filename) }, next)
expect(asked).toBe(shouldAsk)
}),
)