import fs from "fs/promises" import path from "path" import { describe, expect } from "bun:test" import { Effect, Exit, Layer } from "effect" import { FileSystem } from "@opencode-ai/core/filesystem" import { FSUtil } from "@opencode-ai/core/fs-util" import { Location } from "@opencode-ai/core/location" import { Ripgrep } from "@opencode-ai/core/ripgrep" import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema" import { location } from "./fixture/location" import { tmpdir } from "./fixture/tmpdir" import { it } from "./lib/effect" const provide = (directory: string) => Effect.provide( FileSystem.layer.pipe( Layer.provide( Layer.mergeAll( FSUtil.defaultLayer, Ripgrep.defaultLayer, Layer.succeed(Location.Service, Location.Service.of(location({ directory: AbsolutePath.make(directory) }))), ), ), ), ) const withTmp = (f: (directory: string) => Effect.Effect) => Effect.acquireRelease( Effect.promise(() => tmpdir()), (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()), ).pipe(Effect.flatMap((tmp) => f(tmp.path))) describe("FileSystem", () => { it.live("reads text and binary files", () => withTmp((directory) => Effect.gen(function* () { yield* Effect.promise(() => fs.writeFile(path.join(directory, "text.txt"), "hello")) yield* Effect.promise(() => fs.writeFile(path.join(directory, "data.bin"), Buffer.from([0, 1, 2]))) const service = yield* FileSystem.Service const text = yield* service.read({ path: RelativePath.make("text.txt") }) const binary = yield* service.read({ path: RelativePath.make("data.bin") }) expect(new TextDecoder().decode(text.content)).toBe("hello") expect(text.mime).toBe("text/plain") expect(binary.content).toEqual(new Uint8Array([0, 1, 2])) }).pipe(provide(directory)), ), ) it.live("lists direct children", () => withTmp((directory) => Effect.gen(function* () { yield* Effect.promise(() => fs.mkdir(path.join(directory, "src"))) yield* Effect.promise(() => fs.writeFile(path.join(directory, "README.md"), "# Test")) const entries = yield* (yield* FileSystem.Service).list() expect(entries.map((entry) => ({ path: entry.path, type: entry.type }))).toEqual([ { path: RelativePath.make("src" + path.sep), type: "directory" }, { path: RelativePath.make("README.md"), type: "file" }, ]) }).pipe(provide(directory)), ), ) it.live("rejects lexical escapes", () => withTmp((directory) => Effect.gen(function* () { const result = yield* (yield* FileSystem.Service) .read({ path: RelativePath.make("../outside.txt") }) .pipe(Effect.exit) expect(Exit.isFailure(result)).toBe(true) }).pipe(provide(directory)), ), ) })