| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724 |
- import { beforeEach, describe, expect } from "bun:test"
- import path from "path"
- import { Effect, Exit, Layer, PlatformError } from "effect"
- import { Config } from "@opencode-ai/core/config"
- import { ConfigAttachments } from "@opencode-ai/core/config/attachments"
- import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
- import { LayerNode } from "@opencode-ai/util/effect/layer-node"
- import { FileSystem } from "@opencode-ai/core/filesystem"
- import { FSUtil } from "@opencode-ai/util/fs-util"
- import { Location } from "@opencode-ai/core/location"
- import { Image } from "@opencode-ai/core/image"
- import { PermissionV2 } from "@opencode-ai/core/permission"
- import { SessionV2 } from "@opencode-ai/core/session"
- import { AbsolutePath } from "@opencode-ai/core/schema"
- import { Global } from "@opencode-ai/util/global"
- import { LocationMutation } from "@opencode-ai/core/location-mutation"
- import { location } from "./fixture/location"
- import { ToolRegistry } from "@opencode-ai/core/tool/registry"
- import { ToolOutputStore } from "@opencode-ai/core/tool-output-store"
- import { ReadTool } from "@opencode-ai/core/tool/read"
- import { ReadToolFileSystem } from "@opencode-ai/core/tool/read-filesystem"
- import { makeLocationNode } from "@opencode-ai/util/effect/app-node"
- import { SessionInstructions } from "@opencode-ai/core/session/instructions"
- import { testEffect } from "./lib/effect"
- import { toolIdentity, executeTool, registerToolPlugin, settleTool, toolDefinitions } from "./lib/tool"
- const readToolNode = makeLocationNode({
- name: "test/read-tool-plugin",
- layer: Layer.effectDiscard(registerToolPlugin(ReadTool.Plugin)),
- deps: [
- ToolRegistry.toolsNode,
- ReadToolFileSystem.node,
- LocationMutation.node,
- Image.node,
- PermissionV2.node,
- SessionInstructions.node,
- FSUtil.node,
- Location.node,
- ],
- })
- const assertions: PermissionV2.AssertInput[] = []
- const missingPath = "__missing_read_target__.txt"
- const missingAbsolutePath = path.join(process.cwd(), missingPath)
- const readCalls: {
- input: AbsolutePath
- page: ReadToolFileSystem.PageInput
- }[] = []
- const listCalls: ReadToolFileSystem.PageInput[] = []
- let resolvedType: "file" | "directory" = "file"
- let resolveFailure: unknown
- let readResult: FileSystem.Content | ReadToolFileSystem.TextPage = {
- uri: "file:///README.md",
- name: "README.md",
- content: "hello",
- encoding: "utf8",
- mime: "text/plain",
- }
- let readFailure: ReadToolFileSystem.ReadError | undefined
- let configEntries: Config.Entry[] = []
- const reader = Layer.succeed(
- ReadToolFileSystem.Service,
- ReadToolFileSystem.Service.of({
- inspect: () => (resolveFailure === undefined ? Effect.succeed(resolvedType) : Effect.die(resolveFailure)),
- read: (input, _resource, page = {}) => {
- readCalls.push({ input, page })
- if (readFailure !== undefined) return Effect.fail(readFailure)
- return Effect.succeed(readResult)
- },
- list: (_path, input = {}) =>
- Effect.sync(() => {
- listCalls.push(input)
- return new ReadToolFileSystem.ListPage({ entries: [], truncated: false })
- }),
- }),
- )
- let allow = true
- const permission = Layer.succeed(
- PermissionV2.Service,
- PermissionV2.Service.of({
- assert: (input) =>
- Effect.sync(() => {
- assertions.push(input)
- }).pipe(
- Effect.andThen(
- allow
- ? Effect.void
- : Effect.fail(
- new PermissionV2.BlockedError({
- rules: [],
- permission: input.action,
- resources: input.resources,
- }),
- ),
- ),
- ),
- ask: () => Effect.die("unused"),
- reply: () => Effect.die("unused"),
- get: () => Effect.die("unused"),
- forSession: () => Effect.die("unused"),
- list: () => Effect.die("unused"),
- }),
- )
- const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () => Effect.succeed(configEntries) }))
- const imageLayer = AppNodeBuilder.build(Image.node, [[Config.node, config]])
- const testFileSystem = Layer.effect(
- FSUtil.Service,
- FSUtil.Service.use((fs) =>
- Effect.succeed(
- FSUtil.Service.of({
- ...fs,
- realPath: (path) =>
- path === missingAbsolutePath
- ? Effect.fail(
- PlatformError.systemError({
- _tag: "NotFound",
- module: "FileSystem",
- method: "realPath",
- pathOrDescriptor: path,
- }),
- )
- : Effect.succeed(path),
- }),
- ),
- ),
- ).pipe(Layer.provide(LayerNode.compile(FSUtil.node)))
- const locationLayer = Layer.succeed(
- Location.Service,
- Location.Service.of(location({ directory: AbsolutePath.make(process.cwd()) })),
- )
- const mutation = Layer.succeed(
- LocationMutation.Service,
- LocationMutation.Service.of({
- resolve: (input) => {
- if (input.path === missingPath)
- return Effect.fail(new LocationMutation.PathError({ path: input.path, reason: "non_directory_ancestor" }))
- const canonical = path.resolve(process.cwd(), input.path)
- const external = path.isAbsolute(input.path) && !FSUtil.contains(process.cwd(), canonical)
- const resource = external ? canonical.replaceAll("\\", "/") : path.relative(process.cwd(), canonical) || "."
- const directory = path.dirname(canonical)
- const externalResource = path.join(directory, "*").replaceAll("\\", "/")
- return Effect.succeed({
- canonical,
- resource,
- externalDirectory: external
- ? {
- action: "external_directory" as const,
- directory,
- resource: externalResource,
- save: externalResource,
- }
- : undefined,
- })
- },
- }),
- )
- const unavailableImage = Layer.succeed(
- Image.Service,
- Image.Service.of({ normalize: () => Effect.fail(new Image.ResizerUnavailableError()) }),
- )
- const readLayer = (imageLayer: Layer.Layer<Image.Service>) =>
- AppNodeBuilder.build(LayerNode.group([ToolRegistry.node, ToolRegistry.toolsNode, readToolNode]), [
- [ReadToolFileSystem.node, reader],
- [PermissionV2.node, permission],
- [Config.node, config],
- [Image.node, imageLayer],
- [LocationMutation.node, mutation],
- [FSUtil.node, testFileSystem],
- [Location.node, locationLayer],
- [Global.node, Global.layerWith({ data: Global.Path.data })],
- [ToolOutputStore.node, ToolOutputStore.nodeWithoutConfig],
- ])
- const it = testEffect(readLayer(imageLayer))
- const itWithoutResizer = testEffect(readLayer(unavailableImage))
- const sessionID = SessionV2.ID.make("ses_read_tool_test")
- describe("ReadTool", () => {
- beforeEach(() => {
- assertions.length = 0
- readCalls.length = 0
- listCalls.length = 0
- allow = true
- resolvedType = "file"
- resolveFailure = undefined
- readResult = {
- uri: "file:///README.md",
- name: "README.md",
- content: "hello",
- encoding: "utf8",
- mime: "text/plain",
- }
- readFailure = undefined
- configEntries = []
- })
- it.effect("registers, authorizes, and reads through the location filesystem", () =>
- Effect.gen(function* () {
- const registry = yield* ToolRegistry.Service
- expect(yield* toolDefinitions(registry)).toMatchObject([{ name: "read" }])
- expect(yield* toolDefinitions(registry, [{ action: "read", resource: "*", effect: "deny" }])).toEqual([])
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md" } },
- }),
- ).toEqual({
- type: "json",
- value: {
- uri: "file:///README.md",
- name: "README.md",
- content: "hello",
- encoding: "utf8",
- mime: "text/plain",
- },
- })
- expect(assertions).toMatchObject([{ sessionID, action: "read", resources: ["README.md"], save: ["*"] }])
- expect(readCalls).toEqual([
- {
- input: AbsolutePath.make(path.join(process.cwd(), "README.md")),
- page: { offset: undefined, limit: undefined },
- },
- ])
- }),
- )
- it.effect("asks for external_directory approval before reading an external absolute path", () =>
- Effect.gen(function* () {
- const registry = yield* ToolRegistry.Service
- const external = path.join(path.parse(process.cwd()).root, "external-read", "notes.txt")
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-external-read", name: "read", input: { path: external } },
- }),
- ).toMatchObject({ type: "json" })
- expect(assertions).toMatchObject([
- {
- sessionID,
- action: "external_directory",
- resources: [path.join(path.dirname(external), "*").replaceAll("\\", "/")],
- },
- { sessionID, action: "read", resources: [external.replaceAll("\\", "/")], save: ["*"] },
- ])
- expect(readCalls).toEqual([{ input: AbsolutePath.make(external), page: { offset: undefined, limit: undefined } }])
- }),
- )
- it.effect("returns a small PNG as native media instead of durable base64 text", () =>
- Effect.gen(function* () {
- const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
- readResult = {
- uri: "file:///pixel.png",
- name: "pixel.png",
- content: png,
- encoding: "base64",
- mime: "image/png",
- }
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-image", name: "read", input: { path: "pixel.png" } },
- }),
- ).toEqual({
- type: "content",
- value: [
- { type: "text", text: "Image read successfully" },
- { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png", name: "pixel.png" },
- ],
- })
- expect(readCalls).toEqual([
- {
- input: AbsolutePath.make(path.join(process.cwd(), "pixel.png")),
- page: { offset: undefined, limit: undefined },
- },
- ])
- const settled = yield* settleTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-image-settle", name: "read", input: { path: "pixel.png" } },
- })
- expect(settled.output?.structured).toMatchObject({
- uri: "file:///pixel.png",
- name: "pixel.png",
- mime: "image/png",
- encoding: "base64",
- // Image base64 is carried by the content file item only; structured is slimmed
- // so the original bytes are never persisted twice.
- content: "",
- })
- expect(settled.output?.content).toMatchObject([
- { type: "text", text: "Image read successfully" },
- { type: "file", mime: "image/png", uri: `data:image/png;base64,${png}` },
- ])
- }),
- )
- it.effect("preserves a PNG above the generic text limit as native media", () =>
- Effect.gen(function* () {
- const photon = yield* Effect.promise(() => import("@silvia-odwyer/photon-node"))
- const pixels = Uint8Array.from({ length: 256 * 256 * 4 }, (_, index) => (index * 73 + (index >> 3)) % 256)
- const source = new photon.PhotonImage(pixels, 256, 256)
- const png = Buffer.from(source.get_bytes()).toString("base64")
- source.free()
- expect(Buffer.byteLength(png)).toBeGreaterThan(50 * 1024)
- readResult = {
- uri: "file:///large.png",
- name: "large.png",
- content: png,
- encoding: "base64",
- mime: "image/png",
- }
- const registry = yield* ToolRegistry.Service
- const settled = yield* settleTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-large-image", name: "read", input: { path: "large.png" } },
- })
- expect(settled.outputPaths).toBeUndefined()
- expect(settled.output?.structured).toMatchObject({
- uri: "file:///large.png",
- name: "large.png",
- mime: "image/png",
- encoding: "base64",
- })
- expect(settled.result).toEqual({
- type: "content",
- value: [
- { type: "text", text: "Image read successfully" },
- { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png", name: "large.png" },
- ],
- })
- }),
- )
- itWithoutResizer.effect("returns the original image when the resizer is unavailable", () =>
- Effect.gen(function* () {
- const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
- readResult = {
- uri: "file:///pixel.png",
- name: "pixel.png",
- content: png,
- encoding: "base64",
- mime: "image/png",
- }
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-image-fallback", name: "read", input: { path: "pixel.png" } },
- }),
- ).toMatchObject({
- type: "content",
- value: [{ type: "text" }, { type: "file", uri: `data:image/png;base64,${png}`, mime: "image/png" }],
- })
- }),
- )
- it.effect("drops undecodable image data at settlement", () =>
- Effect.gen(function* () {
- readResult = {
- uri: "file:///truncated.png",
- name: "truncated.png",
- content: "iVBORw0KGgo=",
- encoding: "base64",
- mime: "image/png",
- }
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-truncated-image", name: "read", input: { path: "truncated.png" } },
- }),
- ).toEqual({
- type: "content",
- value: [
- { type: "text", text: "Image read successfully" },
- { type: "text", text: "[1 image omitted: could not be decoded.]" },
- ],
- })
- }),
- )
- it.effect("drops oversized images at settlement 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()
- readResult = {
- uri: "file:///wide.png",
- name: "wide.png",
- content: base64,
- encoding: "base64",
- mime: "image/png",
- }
- 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
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-wide-image", name: "read", input: { path: "wide.png" } },
- }),
- ).toEqual({
- type: "content",
- value: [
- { type: "text", text: "Image read successfully" },
- { type: "text", text: "[1 image omitted: could not be resized below the image size limit.]" },
- ],
- })
- }),
- )
- 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()
- readResult = {
- uri: "file:///wide.png",
- name: "wide.png",
- content: base64,
- encoding: "base64",
- mime: "image/png",
- }
- 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* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- 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("file")
- if (media?.type !== "file") return
- const resized = photon.PhotonImage.new_from_byteslice(Buffer.from(media.uri.split(",")[1] ?? "", "base64"))
- expect(resized.get_width()).toBeLessThanOrEqual(4)
- expect(resized.get_height()).toBeLessThanOrEqual(2_000)
- resized.free()
- }),
- )
- it.effect("drops images that cannot fit max base64 bytes after resize attempts", () =>
- Effect.gen(function* () {
- const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
- readResult = {
- uri: "file:///pixel.png",
- name: "pixel.png",
- content: png,
- encoding: "base64",
- mime: "image/png",
- }
- 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
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-max-bytes", name: "read", input: { path: "pixel.png" } },
- }),
- ).toEqual({
- type: "content",
- value: [
- { type: "text", text: "Image read successfully" },
- { type: "text", text: "[1 image omitted: could not be resized below the image size limit.]" },
- ],
- })
- }),
- )
- it.effect("returns supported image contents despite a misleading binary extension", () =>
- Effect.gen(function* () {
- const png = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII="
- readResult = {
- uri: "file:///pixel.bin",
- name: "pixel.bin",
- content: png,
- encoding: "base64",
- mime: "image/png",
- }
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-disguised-image", name: "read", input: { path: "pixel.bin" } },
- }),
- ).toMatchObject({
- type: "content",
- value: [{ type: "text" }, { type: "file", mime: "image/png", name: "pixel.bin" }],
- })
- }),
- )
- it.effect("returns expected filesystem failures to the model", () =>
- Effect.gen(function* () {
- readFailure = new ReadToolFileSystem.BinaryFileError({ resource: "archive.dat" })
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- 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(readCalls).toEqual([
- { input: AbsolutePath.make(path.join(process.cwd(), "archive.dat")), page: { offset: 2, limit: 1 } },
- ])
- }),
- )
- it.effect("preserves unexpected filesystem defects", () =>
- Effect.gen(function* () {
- resolveFailure = new Error("unexpected")
- const registry = yield* ToolRegistry.Service
- expect(
- Exit.isFailure(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-defect", name: "read", input: { path: "README.md" } },
- }).pipe(Effect.exit),
- ),
- ).toBe(true)
- }),
- )
- it.effect("does not read when permission is denied", () =>
- Effect.gen(function* () {
- allow = false
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-read", name: "read", input: { path: "README.md" } },
- }),
- ).toEqual({ type: "error", value: "Unable to read README.md" })
- expect(readCalls).toEqual([])
- }),
- )
- it.effect("returns missing paths as model-visible tool failures", () =>
- Effect.gen(function* () {
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-missing-path", name: "read", input: { path: missingPath } },
- }),
- ).toEqual({ type: "error", value: `Unable to read ${missingPath}` })
- expect(assertions).toEqual([])
- expect(readCalls).toEqual([])
- }),
- )
- it.effect("lists a bounded directory page through read", () =>
- Effect.gen(function* () {
- resolvedType = "directory"
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- 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(listCalls).toEqual([{ offset: 2, limit: 10 }])
- }),
- )
- it.effect("does not list a directory when permission is denied", () =>
- Effect.gen(function* () {
- allow = false
- resolvedType = "directory"
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-read-directory-denied", name: "read", input: { path: "src" } },
- }),
- ).toEqual({ type: "error", value: "Unable to read src" })
- expect(listCalls).toEqual([])
- }),
- )
- it.effect("preserves unexpected resolution defects", () =>
- Effect.gen(function* () {
- const registry = yield* ToolRegistry.Service
- resolveFailure = new Error("missing")
- expect(
- Exit.isFailure(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-missing", name: "read", input: { path: "missing.txt" } },
- }).pipe(Effect.exit),
- ),
- ).toBe(true)
- expect(readCalls).toEqual([])
- }),
- )
- it.effect("forwards pagination and returns bounded text pages with continuation", () =>
- Effect.gen(function* () {
- readResult = new ReadToolFileSystem.TextPage({
- type: "text-page",
- content: "hello",
- mime: "text/plain",
- offset: 2,
- truncated: true,
- next: 3,
- })
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- 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(readCalls).toEqual([
- { input: AbsolutePath.make(path.join(process.cwd(), "large.txt")), page: { offset: 2, limit: 1 } },
- ])
- }),
- )
- it.effect("rejects unsupported binary discovered by a direct read", () =>
- Effect.gen(function* () {
- readResult = {
- uri: "file:///late-binary",
- name: "late-binary",
- content: "AAECAw==",
- encoding: "base64",
- mime: "application/octet-stream",
- }
- const registry = yield* ToolRegistry.Service
- expect(
- yield* executeTool(registry, {
- sessionID,
- ...toolIdentity,
- call: { type: "tool-call", id: "call-direct-binary", name: "read", input: { path: "late-binary" } },
- }),
- ).toEqual({ type: "error", value: "Cannot read binary file: late-binary" })
- }),
- )
- })
|