mirror of
https://github.com/anomalyco/opencode.git
synced 2026-11-13 19:46:16 +03:00
refactor(core): type directory initialization errors (#52649)
This commit is contained in:
committed by
GitHub
Unverified
parent
37a426e7f4
commit
1404566141
@@ -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))
|
||||
|
||||
@@ -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* () {
|
||||
|
||||
Reference in New Issue
Block a user