opencode/packages/core/test/location-filesystem.test.ts
Kit Langton 36ac35a7c8
refactor(util): make layer graphs opaque and composable
Replace exposed layer graph assembly with opaque declarations, checked substitutions, and lifetime-aware compilation. Preserve deep replacement, ordered startup, and Effect-owned resource lifetimes; migrate callers and verify source and published package contracts.
2026-08-31 13:46:27 -04:00

114 lines
4.7 KiB
TypeScript

import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Effect, Exit, Layer } from "effect"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
import { FileSystem } from "@opencode-ai/core/filesystem"
import { Location } from "@opencode-ai/core/location"
import { AbsolutePath, RelativePath } from "@opencode-ai/core/schema"
import { Workspace } from "@opencode-ai/core/workspace"
import { location } from "./fixture/location"
import { tmpdir } from "./fixture/tmpdir"
import { it } from "./lib/effect"
const provide = (directory: string, workspaceID?: Workspace.ID) =>
Effect.provide(
LayerNode.compile(FileSystem.node, {
replacements: [
Location.node.replace(
Layer.succeed(
Location.Service,
Location.Service.of(location({ directory: AbsolutePath.make(directory), workspaceID })),
),
),
],
}),
)
const withTmp = <A, E, R>(f: (directory: string) => Effect.Effect<A, E, R>) =>
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 filesystem = yield* FileSystem.Service
const entries = yield* filesystem.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("skips host canonicalization for workspace locations at boot", () =>
withTmp((directory) =>
Effect.gen(function* () {
// The directory exists only inside the workspace, so boot must not
// require it to exist on the host. Operations still access the host
// filesystem per call (#44568); only boot canonicalization is skipped.
const missing = path.join(directory, "workspace-only")
const workspace = yield* FileSystem.Service.pipe(
provide(missing, Workspace.ID.make("wrk_filesystem")),
Effect.exit,
)
expect(Exit.isSuccess(workspace)).toBe(true)
// A local ref with the same missing directory keeps failing boot:
// 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)
}),
),
)
it.live("canonicalizes local symlinked directories", () =>
withTmp((directory) =>
Effect.gen(function* () {
const real = path.join(directory, "real")
yield* Effect.promise(() => fs.mkdir(real))
yield* Effect.promise(() => fs.writeFile(path.join(real, "file.txt"), "linked"))
const link = path.join(directory, "link")
yield* Effect.promise(() => fs.symlink(real, link))
// Reads resolve through the symlink only because boot canonicalized
// the location root to the real directory.
const read = yield* FileSystem.Service.pipe(
Effect.flatMap((service) => service.read({ path: RelativePath.make("file.txt") })),
provide(link),
)
expect(new TextDecoder().decode(read.content)).toBe("linked")
}),
),
)
it.live("rejects lexical escapes", () =>
withTmp((directory) =>
Effect.gen(function* () {
const filesystem = yield* FileSystem.Service
const result = yield* filesystem.read({ path: RelativePath.make("../outside.txt") }).pipe(Effect.exit)
expect(Exit.isFailure(result)).toBe(true)
}).pipe(provide(directory)),
),
)
})