refactor(core): type directory initialization errors (#52649)

This commit is contained in:
Aiden Cline
2026-10-01 22:45:08 -05:00
committed by GitHub
Unverified
parent 37a426e7f4
commit 1404566141
2 changed files with 110 additions and 2 deletions
+42 -1
View File
@@ -26,6 +26,30 @@ export class NotFoundError extends Schema.TaggedError<NotFoundError>()("FileSyst
path: RelativePath,
}) {}
export class DirectoryNotFoundError extends Schema.TaggedError<DirectoryNotFoundError>()(
"FileSystem.DirectoryNotFoundError",
{
directory: AbsolutePath,
cause: Schema.Defect(),
},
) {
override get message() {
return `Directory not found: ${this.directory}`
}
}
export class DirectoryAccessDeniedError extends Schema.TaggedError<DirectoryAccessDeniedError>()(
"FileSystem.DirectoryAccessDeniedError",
{
directory: AbsolutePath,
cause: Schema.Defect(),
},
) {
override get message() {
return `Access denied to directory: ${this.directory}`
}
}
export const Content = Schema.Struct({
uri: Schema.String,
name: Schema.String.pipe(Schema.optional),
@@ -87,7 +111,24 @@ const baseLayer = Layer.effect(
// configured directory as canonical; local placements keep symlink
// canonicalization. This skip is boot-only: resolve/read/list below still
// access the host filesystem per operation (tracked in #44568).
const root = location.workspaceID ? location.directory : yield* fs.realPath(location.directory).pipe(Effect.orDie)
const root = location.workspaceID
? location.directory
: yield* fs.realPath(location.directory).pipe(
Effect.catch((cause): Effect.Effect<never, DirectoryNotFoundError | DirectoryAccessDeniedError> => {
if (cause.reason._tag === "NotFound")
return Effect.fail(new DirectoryNotFoundError({ directory: location.directory, cause }))
// macOS privacy denials arrive as Unknown with an EPERM cause.
if (
cause.reason._tag === "PermissionDenied" ||
(cause.reason._tag === "Unknown" &&
cause.reason.cause instanceof Error &&
"code" in cause.reason.cause &&
cause.reason.cause.code === "EPERM")
)
return Effect.fail(new DirectoryAccessDeniedError({ directory: location.directory, cause }))
return Effect.die(cause)
}),
)
const resolve = Effect.fnUntraced(function* (input?: RelativePath) {
const absolute = path.resolve(location.directory, input ?? ".")
if (!FSUtil.contains(location.directory, absolute))
+68 -1
View File
@@ -1,7 +1,8 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Exit, Layer } from "effect"
import { Cause, Effect, Exit, Layer, PlatformError } from "effect"
import { FSUtil } from "@opencode/util/fs-util"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { FileSystem } from "@opencode/core/filesystem"
import { Location } from "@opencode/core/location"
@@ -79,10 +80,76 @@ describe("FileSystem", () => {
// host realpath canonicalization stays load-bearing for local placements.
const local = yield* FileSystem.Service.pipe(provide(missing), Effect.exit)
expect(Exit.isFailure(local)).toBe(true)
if (Exit.isFailure(local)) {
const error = Cause.findErrorOption(local.cause)
expect(error).toMatchObject({
_tag: "Some",
value: {
_tag: "FileSystem.DirectoryNotFoundError",
directory: missing,
message: `Directory not found: ${missing}`,
},
})
}
}),
),
)
for (const input of [
{ reason: "PermissionDenied", code: "EACCES", denied: true },
{ reason: "Unknown", code: "EPERM", denied: true },
{ reason: "Unknown", code: "EIO", denied: false },
] as const) {
it.live(`classifies directory initialization failure ${input.code}`, () =>
withTmp((directory) =>
Effect.gen(function* () {
const filesystem = yield* FSUtil.Service
const cause = PlatformError.systemError({
_tag: input.reason,
module: "FileSystem",
method: "realPath",
pathOrDescriptor: directory,
cause: Object.assign(new Error(input.code), { code: input.code }),
})
const result = yield* FileSystem.Service.pipe(
Effect.provide(
LayerNode.compile(FileSystem.node, {
replacements: [
Location.node.replace(
Layer.succeed(Location.Service, location({ directory: AbsolutePath.make(directory) })),
),
FSUtil.node.replace(
Layer.succeed(FSUtil.Service, {
...filesystem,
realPath: (target) => (target === directory ? Effect.fail(cause) : filesystem.realPath(target)),
}),
),
],
}),
),
Effect.exit,
)
expect(Exit.isFailure(result)).toBe(true)
if (Exit.isFailure(result)) {
if (input.denied) {
expect(Cause.findErrorOption(result.cause)).toMatchObject({
_tag: "Some",
value: {
_tag: "FileSystem.DirectoryAccessDeniedError",
directory,
cause,
message: `Access denied to directory: ${directory}`,
},
})
return
}
expect(result.cause.reasons.filter(Cause.isDieReason)).toMatchObject([{ defect: cause }])
}
}).pipe(Effect.provide(LayerNode.compile(FSUtil.node))),
),
)
}
it.live("lists parents and siblings with paths relative to the current location", () =>
withTmp((directory) =>
Effect.gen(function* () {