فهرست منبع

fix(core): handle missing read paths (#33255)

Kit Langton 2 ماه پیش
والد
کامیت
823d327401
2فایلهای تغییر یافته به همراه39 افزوده شده و 4 حذف شده
  1. 2 2
      packages/core/src/tool/read.ts
  2. 37 2
      packages/core/test/tool-read.test.ts

+ 2 - 2
packages/core/src/tool/read.ts

@@ -57,8 +57,8 @@ export const layer = Layer.effectDiscard(
               const selected = path.isAbsolute(input.path) ? path.dirname(absolute) : location.directory
               if (!path.isAbsolute(input.path) && !FSUtil.contains(location.directory, absolute))
                 return yield* Effect.die(new Error("Path escapes the allowed read root"))
-              const real = yield* fs.realPath(absolute).pipe(Effect.orDie)
-              const root = yield* fs.realPath(selected).pipe(Effect.orDie)
+              const real = yield* fs.realPath(absolute)
+              const root = yield* fs.realPath(selected)
               if (!FSUtil.contains(root, real))
                 return yield* Effect.die(new Error("Path escapes the allowed read root"))
               const resource = path.relative(root, real).replaceAll("\\", "/") || "."

+ 37 - 2
packages/core/test/tool-read.test.ts

@@ -1,5 +1,5 @@
 import { beforeEach, describe, expect } from "bun:test"
-import { Effect, Exit, Layer } from "effect"
+import { Effect, Exit, Layer, PlatformError } from "effect"
 import { Config } from "@opencode-ai/core/config"
 import { ConfigAttachments } from "@opencode-ai/core/config/attachments"
 import { FileSystem } from "@opencode-ai/core/filesystem"
@@ -18,6 +18,8 @@ import { testEffect } from "./lib/effect"
 import { toolIdentity, executeTool, settleTool, toolDefinitions } from "./lib/tool"
 
 const assertions: PermissionV2.AssertInput[] = []
+const missingPath = "__missing_read_target__.txt"
+const missingAbsolutePath = `${process.cwd()}/${missingPath}`
 const readCalls: {
   input: AbsolutePath
   page: ReadToolFileSystem.PageInput
@@ -70,7 +72,24 @@ const config = Layer.succeed(Config.Service, Config.Service.of({ entries: () =>
 const image = Image.layer.pipe(Layer.provide(config))
 const testFileSystem = Layer.effect(
   FSUtil.Service,
-  FSUtil.Service.use((fs) => Effect.succeed(FSUtil.Service.of({ ...fs, realPath: (path) => Effect.succeed(path) }))),
+  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(FSUtil.defaultLayer))
 const infrastructure = Layer.mergeAll(
   testFileSystem,
@@ -453,6 +472,22 @@ describe("ReadTool", () => {
     }),
   )
 
+  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"