| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747 |
- import { describe, expect } from "bun:test"
- import { Effect, Layer } from "effect"
- import { Config } from "@opencode-ai/core/config"
- import { ConfigAttachments } from "@opencode-ai/core/config/attachments"
- import { FileSystem } from "@opencode-ai/core/filesystem"
- import { PermissionV2 } from "@opencode-ai/core/permission"
- import { SessionV2 } from "@opencode-ai/core/session"
- import { ToolRegistry } from "@opencode-ai/core/tool/registry"
- import { ReadTool } from "@opencode-ai/core/tool/read"
- import { RelativePath } from "@opencode-ai/core/schema"
- import { testEffect } from "./lib/effect"
- const assertions: PermissionV2.AssertInput[] = []
- const reads: FileSystem.ReadInput[] = []
- const samples: number[] = []
- const textPageInputs: FileSystem.TextPageInput[] = []
- const pages: FileSystem.ListTarget[] = []
- const pageInputs: Pick<FileSystem.ListPageInput, "offset" | "limit">[] = []
- let resolvedInput: FileSystem.ReadInput | undefined
- let resolveFailure: unknown
- let listResolveFailure: unknown = new Error("not a directory")
- let listReal = "/project/src"
- let size = 5
- let real = "/project/README.md"
- let afterApproval = () => {}
- let readContent: FileSystem.Content = new FileSystem.TextContent({
- type: "text",
- content: "hello",
- mime: "text/plain",
- })
- let sample = new TextEncoder().encode("hello")
- let readFailure: unknown
- let configEntries: Config.Entry[] = []
- const filesystem = Layer.succeed(
- FileSystem.Service,
- FileSystem.Service.of({
- read: () => Effect.die("unused"),
- resolveReadPath: (input) =>
- resolveFailure === undefined
- ? Effect.succeed({
- type: "file" as const,
- target: new FileSystem.ReadTarget({
- real,
- resource: input.reference === undefined ? input.path : `${input.reference}:${input.path}`,
- size,
- dev: 1,
- }),
- })
- : listResolveFailure === undefined
- ? Effect.succeed({
- type: "directory" as const,
- target: new FileSystem.ListTarget({
- absolute: `/project/${input.path ?? "."}`,
- real: listReal,
- directory: "/project",
- root: "/project",
- resource: input.path ?? ".",
- }),
- })
- : Effect.die(resolveFailure),
- resolveRead: (input) =>
- Effect.sync(() => {
- resolvedInput = input
- }).pipe(
- Effect.andThen(
- resolveFailure === undefined
- ? Effect.succeed(
- new FileSystem.ReadTarget({
- real,
- resource: input.reference === undefined ? input.path : `${input.reference}:${input.path}`,
- size,
- dev: 1,
- }),
- )
- : Effect.die(resolveFailure),
- ),
- ),
- readResolved: () =>
- readFailure === undefined
- ? Effect.sync(() => {
- reads.push({ path: RelativePath.make("README.md") })
- return readContent
- })
- : Effect.die(readFailure),
- readSampleResolved: (_target, maximumBytes) =>
- Effect.sync(() => {
- samples.push(maximumBytes)
- return sample.slice(0, maximumBytes)
- }),
- readTextPageResolved: (_target, page = {}) =>
- readFailure === undefined
- ? Effect.sync(() => {
- textPageInputs.push(page)
- return new FileSystem.TextPage({
- type: "text-page",
- content: "hello",
- mime: "text/plain",
- offset: page.offset ?? 1,
- truncated: true,
- next: (page.offset ?? 1) + 1,
- })
- })
- : Effect.die(readFailure),
- readToolResolved: (_target, page = {}) => {
- samples.push(FileSystem.READ_SAMPLE_BYTES)
- if (readFailure !== undefined) return Effect.die(readFailure)
- if (sample[0] === 0x89 && sample[1] === 0x50 && sample[2] === 0x4e && sample[3] === 0x47)
- return Effect.succeed(
- readContent.type === "binary"
- ? new FileSystem.BinaryContent({ ...readContent, mime: "image/png" })
- : readContent,
- )
- if (FileSystem.isBinary(real.split("/").at(-1) ?? real, sample))
- return Effect.die(new FileSystem.BinaryFileError(real.split("/").at(-1) ?? real))
- if (size > FileSystem.MAX_READ_BYTES || page.offset !== undefined || page.limit !== undefined)
- return Effect.sync(() => {
- textPageInputs.push(page)
- return new FileSystem.TextPage({
- type: "text-page",
- content: "hello",
- mime: "text/plain",
- offset: page.offset ?? 1,
- truncated: true,
- next: (page.offset ?? 1) + 1,
- })
- })
- return Effect.sync(() => {
- reads.push({ path: RelativePath.make("README.md") })
- return readContent
- })
- },
- resolveRoot: () => Effect.die("unused"),
- revalidateRoot: Effect.succeed,
- list: () => Effect.die("unused"),
- resolveList: (input = {}) =>
- listResolveFailure === undefined
- ? Effect.succeed(
- new FileSystem.ListTarget({
- absolute: `/project/${input.path ?? "."}`,
- real: listReal,
- directory: "/project",
- root: "/project",
- resource: input.path ?? ".",
- }),
- )
- : Effect.die(listResolveFailure),
- listResolved: () => Effect.die("unused"),
- listPage: () => Effect.die("unused"),
- listPageResolved: (target, page = {}) =>
- Effect.sync(() => {
- pages.push(target)
- pageInputs.push(page)
- return new FileSystem.ListPage({ entries: [], truncated: false })
- }),
- find: () => Effect.die("unused"),
- grep: () => Effect.die("unused"),
- isIgnored: () => false,
- }),
- )
- let allow = true
- const permission = Layer.succeed(
- PermissionV2.Service,
- PermissionV2.Service.of({
- assert: (input) =>
- Effect.sync(() => {
- assertions.push(input)
- if (allow) afterApproval()
- }).pipe(Effect.andThen(allow ? Effect.void : Effect.fail(new PermissionV2.DeniedError({ rules: [] })))),
- ask: () => Effect.die("unused"),
- reply: () => Effect.die("unused"),
- get: () => Effect.die("unused"),
- forSession: () => Effect.die("unused"),
- list: () => Effect.die("unused"),
- }),
- )
- const registry = ToolRegistry.defaultLayer.pipe(Layer.provide(permission))
- const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed(configEntries) }))
- const read = ReadTool.layer.pipe(
- Layer.provide(registry),
- Layer.provide(filesystem),
- Layer.provide(permission),
- Layer.provide(config),
- )
- const it = testEffect(Layer.mergeAll(registry, filesystem, permission, config, read))
- const sessionID = SessionV2.ID.make("ses_read_tool_test")
- describe("ReadTool", () => {
- it.effect("registers, authorizes, and reads through the location filesystem", () =>
- Effect.gen(function* () {
- assertions.length = 0
- reads.length = 0
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = 5
- real = "/project/README.md"
- afterApproval = () => {}
- readContent = new FileSystem.TextContent({ type: "text", content: "hello", mime: "text/plain" })
- sample = new TextEncoder().encode("hello")
- readFailure = undefined
- configEntries = []
- resolvedInput = undefined
- const registry = yield* ToolRegistry.Service
- expect(yield* registry.definitions()).toMatchObject([{ name: "read" }])
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md" } },
- }),
- ).toEqual({ type: "json", value: { type: "text", content: "hello", mime: "text/plain" } })
- expect(assertions).toMatchObject([{ sessionID, action: "read", resources: ["README.md"], save: ["*"] }])
- expect(reads).toEqual([{ path: RelativePath.make("README.md") }])
- }),
- )
- it.effect("returns a small PNG as native media instead of durable base64 text", () =>
- Effect.gen(function* () {
- const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
- reads.length = 0
- samples.length = 0
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = Buffer.from(png, "base64").length
- real = "/project/pixel.png"
- afterApproval = () => {}
- sample = Buffer.from(png, "base64")
- readContent = new FileSystem.BinaryContent({
- type: "binary",
- content: png,
- encoding: "base64",
- mime: "image/png",
- })
- readFailure = undefined
- configEntries = []
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-image", name: "read", input: { path: "pixel.png" } },
- }),
- ).toEqual({
- type: "content",
- value: [
- { type: "text", text: "Image read successfully" },
- { type: "media", mediaType: "image/png", data: png, filename: "pixel.png" },
- ],
- })
- expect(samples).toEqual([FileSystem.READ_SAMPLE_BYTES])
- expect(reads).toHaveLength(0)
- const settled = yield* registry.settle({
- sessionID,
- call: { type: "tool-call", id: "call-image-settle", name: "read", input: { path: "pixel.png" } },
- })
- expect(settled.output?.structured).toEqual({ type: "media", mime: "image/png" })
- expect(JSON.stringify(settled.output?.structured)).not.toContain(png)
- expect(settled.output?.content).toMatchObject([
- { type: "text", text: "Image read successfully" },
- { type: "file", mime: "image/png", source: { type: "data", data: png } },
- ])
- }),
- )
- it.effect("rejects invalid or truncated image data after signature classification", () =>
- Effect.gen(function* () {
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = 8
- real = "/project/truncated.png"
- afterApproval = () => {}
- sample = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
- readContent = new FileSystem.BinaryContent({
- type: "binary",
- content: Buffer.from(sample).toString("base64"),
- encoding: "base64",
- mime: "image/png",
- })
- readFailure = undefined
- configEntries = []
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-truncated-image", name: "read", input: { path: "truncated.png" } },
- }),
- ).toEqual({ type: "error", value: "Image could not be decoded: truncated.png" })
- }),
- )
- it.effect("rejects oversized images when resizing is disabled", () =>
- Effect.gen(function* () {
- const photon = yield* Effect.promise(() => import("@silvia-odwyer/photon-node"))
- const source = new photon.PhotonImage(new Uint8Array(Array.from({ length: 16 * 4 }, () => 255)), 16, 1)
- const base64 = Buffer.from(source.get_bytes()).toString("base64")
- source.free()
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = Buffer.from(base64, "base64").length
- real = "/project/wide.png"
- afterApproval = () => {}
- sample = Buffer.from(base64, "base64")
- readContent = new FileSystem.BinaryContent({
- type: "binary",
- content: base64,
- encoding: "base64",
- mime: "image/png",
- })
- readFailure = undefined
- configEntries = [
- new Config.Document({
- type: "document",
- info: new Config.Info({
- attachments: new ConfigAttachments.Info({
- image: new ConfigAttachments.Image({ auto_resize: false, max_width: 4 }),
- }),
- }),
- }),
- ]
- const registry = yield* ToolRegistry.Service
- const result = yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-wide-image", name: "read", input: { path: "wide.png" } },
- })
- expect(result.type).toBe("error")
- if (result.type === "error") expect(result.value).toContain("exceeding configured limits 4x2000")
- }),
- )
- it.effect("resizes images to configured dimensions before returning media", () =>
- Effect.gen(function* () {
- const photon = yield* Effect.promise(() => import("@silvia-odwyer/photon-node"))
- const source = new photon.PhotonImage(new Uint8Array(Array.from({ length: 16 * 4 }, () => 255)), 16, 1)
- const base64 = Buffer.from(source.get_bytes()).toString("base64")
- source.free()
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = Buffer.from(base64, "base64").length
- real = "/project/wide.png"
- afterApproval = () => {}
- sample = Buffer.from(base64, "base64")
- readContent = new FileSystem.BinaryContent({
- type: "binary",
- content: base64,
- encoding: "base64",
- mime: "image/png",
- })
- readFailure = undefined
- configEntries = [
- new Config.Document({
- type: "document",
- info: new Config.Info({
- attachments: new ConfigAttachments.Info({ image: new ConfigAttachments.Image({ max_width: 4 }) }),
- }),
- }),
- ]
- const registry = yield* ToolRegistry.Service
- const result = yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-resize-image", name: "read", input: { path: "wide.png" } },
- })
- expect(result.type).toBe("content")
- if (result.type !== "content") return
- const media = result.value[1]
- expect(media?.type).toBe("media")
- if (media?.type !== "media") return
- const resized = photon.PhotonImage.new_from_byteslice(Buffer.from(media.data, "base64"))
- expect(resized.get_width()).toBeLessThanOrEqual(4)
- expect(resized.get_height()).toBeLessThanOrEqual(2_000)
- resized.free()
- }),
- )
- it.effect("enforces max base64 bytes after resize attempts", () =>
- Effect.gen(function* () {
- const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = Buffer.from(png, "base64").length
- real = "/project/pixel.png"
- afterApproval = () => {}
- sample = Buffer.from(png, "base64")
- readContent = new FileSystem.BinaryContent({
- type: "binary",
- content: png,
- encoding: "base64",
- mime: "image/png",
- })
- readFailure = undefined
- configEntries = [
- new Config.Document({
- type: "document",
- info: new Config.Info({
- attachments: new ConfigAttachments.Info({
- image: new ConfigAttachments.Image({ max_base64_bytes: 1 }),
- }),
- }),
- }),
- ]
- const registry = yield* ToolRegistry.Service
- const result = yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-max-bytes", name: "read", input: { path: "pixel.png" } },
- })
- expect(result.type).toBe("error")
- if (result.type === "error") expect(result.value).toContain("/1 bytes")
- }),
- )
- it.effect("classifies supported image contents before a misleading binary extension", () =>
- Effect.gen(function* () {
- const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = Buffer.from(png, "base64").length
- real = "/project/pixel.bin"
- afterApproval = () => {}
- sample = Buffer.from(png, "base64")
- readContent = new FileSystem.BinaryContent({
- type: "binary",
- content: png,
- encoding: "base64",
- mime: "application/octet-stream",
- })
- readFailure = undefined
- configEntries = []
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-disguised-image", name: "read", input: { path: "pixel.bin" } },
- }),
- ).toMatchObject({
- type: "content",
- value: [{ type: "text" }, { type: "media", mediaType: "image/png", filename: "pixel.bin" }],
- })
- }),
- )
- it.effect("rejects unsupported binary before direct reads or paging", () =>
- Effect.gen(function* () {
- reads.length = 0
- textPageInputs.length = 0
- samples.length = 0
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = FileSystem.MAX_READ_BYTES + 1
- real = "/project/archive.dat"
- afterApproval = () => {}
- sample = new Uint8Array([0, 1, 2, 3])
- readFailure = undefined
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: {
- type: "tool-call",
- id: "call-binary",
- name: "read",
- input: { path: "archive.dat", offset: 2, limit: 1 },
- },
- }),
- ).toEqual({ type: "error", value: "Cannot read binary file: archive.dat" })
- expect(samples).toEqual([FileSystem.READ_SAMPLE_BYTES])
- expect(reads).toEqual([])
- expect(textPageInputs).toEqual([])
- }),
- )
- it.effect("does not read when permission is denied", () =>
- Effect.gen(function* () {
- assertions.length = 0
- reads.length = 0
- allow = false
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = 5
- real = "/project/README.md"
- afterApproval = () => {}
- resolvedInput = undefined
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md" } },
- }),
- ).toEqual({ type: "error", value: "Unable to read README.md" })
- expect(reads).toEqual([])
- }),
- )
- it.effect("lists a bounded directory page through read", () =>
- Effect.gen(function* () {
- assertions.length = 0
- pages.length = 0
- pageInputs.length = 0
- allow = true
- resolveFailure = new Error("Path is not a file")
- listResolveFailure = undefined
- listReal = "/project/src"
- afterApproval = () => {}
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: {
- type: "tool-call",
- id: "call-read-directory",
- name: "read",
- input: { path: "src", offset: 2, limit: 10 },
- },
- }),
- ).toEqual({ type: "json", value: { entries: [], truncated: false } })
- expect(assertions).toMatchObject([{ sessionID, action: "read", resources: ["src"], save: ["*"] }])
- expect(pageInputs).toEqual([{ offset: 2, limit: 10 }])
- }),
- )
- it.effect("does not list a directory when permission is denied", () =>
- Effect.gen(function* () {
- pages.length = 0
- allow = false
- resolveFailure = new Error("Path is not a file")
- listResolveFailure = undefined
- listReal = "/project/src"
- afterApproval = () => {}
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-read-directory-denied", name: "read", input: { path: "src" } },
- }),
- ).toEqual({ type: "error", value: "Unable to read src" })
- expect(pages).toEqual([])
- }),
- )
- it.effect("does not list when the directory changes after permission approval", () =>
- Effect.gen(function* () {
- pages.length = 0
- allow = true
- resolveFailure = new Error("Path is not a file")
- listResolveFailure = undefined
- listReal = "/project/src"
- afterApproval = () => {
- listReal = "/outside/src"
- }
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-read-directory-swapped", name: "read", input: { path: "src" } },
- }),
- ).toEqual({ type: "error", value: "Unable to read src" })
- expect(pages).toEqual([])
- }),
- )
- it.effect("authorizes project references with their canonical identity", () =>
- Effect.gen(function* () {
- assertions.length = 0
- reads.length = 0
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = 5
- real = "/project/README.md"
- afterApproval = () => {}
- resolvedInput = undefined
- const registry = yield* ToolRegistry.Service
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md", reference: "docs" } },
- })
- expect(assertions).toMatchObject([{ resources: ["docs:README.md"] }])
- }),
- )
- it.effect("settles missing files as typed tool errors", () =>
- Effect.gen(function* () {
- allow = true
- reads.length = 0
- real = "/project/README.md"
- afterApproval = () => {}
- const registry = yield* ToolRegistry.Service
- resolveFailure = new Error("missing")
- listResolveFailure = new Error("missing")
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-missing", name: "read", input: { path: "missing.txt" } },
- }),
- ).toEqual({ type: "error", value: "Unable to read missing.txt" })
- expect(reads).toEqual([])
- }),
- )
- it.effect("reads large UTF-8 text files as bounded pages with continuation", () =>
- Effect.gen(function* () {
- textPageInputs.length = 0
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = FileSystem.MAX_READ_BYTES + 1
- real = "/project/large.txt"
- afterApproval = () => {}
- sample = new TextEncoder().encode("hello")
- readFailure = undefined
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: {
- type: "tool-call",
- id: "call-large",
- name: "read",
- input: { path: "large.txt", offset: 2, limit: 1 },
- },
- }),
- ).toEqual({
- type: "json",
- value: { type: "text-page", content: "hello", mime: "text/plain", offset: 2, truncated: true, next: 3 },
- })
- expect(textPageInputs).toEqual([{ offset: 2, limit: 1 }])
- }),
- )
- it.effect("preserves safe read limit errors", () =>
- Effect.gen(function* () {
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = 5
- real = "/project/changed.txt"
- afterApproval = () => {}
- sample = new TextEncoder().encode("hello")
- readFailure = new FileSystem.ReadLimitError("changed.txt", FileSystem.MAX_READ_BYTES)
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-read-limit", name: "read", input: { path: "changed.txt" } },
- }),
- ).toEqual({
- type: "error",
- value: `File exceeds ${FileSystem.MAX_READ_BYTES} byte read limit: changed.txt`,
- })
- }),
- )
- it.effect("preserves binary errors discovered after the sample", () =>
- Effect.gen(function* () {
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = FileSystem.MAX_READ_BYTES + 1
- real = "/project/late-binary"
- afterApproval = () => {}
- sample = new TextEncoder().encode("text prefix")
- readFailure = new FileSystem.BinaryFileError("late-binary")
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-late-binary", name: "read", input: { path: "late-binary" } },
- }),
- ).toEqual({ type: "error", value: "Cannot read binary file: late-binary" })
- }),
- )
- it.effect("rejects unsupported binary discovered by a direct read", () =>
- Effect.gen(function* () {
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = 5
- real = "/project/late-binary"
- afterApproval = () => {}
- sample = new TextEncoder().encode("text prefix")
- readFailure = undefined
- readContent = new FileSystem.BinaryContent({
- type: "binary",
- content: "AAECAw==",
- encoding: "base64",
- mime: "application/octet-stream",
- })
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-direct-binary", name: "read", input: { path: "late-binary" } },
- }),
- ).toEqual({ type: "error", value: "Cannot read binary file: late-binary" })
- }),
- )
- it.effect("does not read when the file changes after permission approval", () =>
- Effect.gen(function* () {
- assertions.length = 0
- reads.length = 0
- allow = true
- resolveFailure = undefined
- listResolveFailure = new Error("not a directory")
- size = 5
- real = "/project/README.md"
- sample = new TextEncoder().encode("hello")
- readFailure = undefined
- afterApproval = () => {
- real = "/outside/README.md"
- }
- const registry = yield* ToolRegistry.Service
- expect(
- yield* registry.execute({
- sessionID,
- call: { type: "tool-call", id: "call-swapped", name: "read", input: { path: "README.md" } },
- }),
- ).toEqual({ type: "error", value: "Unable to read README.md" })
- expect(reads).toEqual([])
- }),
- )
- })
|