Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 0 additions & 16 deletions packages/core/test/global.test.ts

This file was deleted.

12 changes: 7 additions & 5 deletions packages/core/test/instruction-discovery.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect } from "bun:test"
import { Effect, Layer } from "effect"
import fs from "fs/promises"
import os from "os"
import path from "path"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
Expand All @@ -15,6 +16,7 @@ import { testEffect } from "./lib/effect"
import { readInitial, readUpdate, state } from "./lib/instructions"

const it = testEffect(Layer.empty)
const testConfig = path.join(os.tmpdir(), "opencode-instruction-discovery-test")

const instructionLayer = (input: {
config: string
Expand Down Expand Up @@ -147,7 +149,7 @@ describe("InstructionDiscovery", () => {
Effect.flatMap((service) => service.load()),
Effect.provide(
instructionLayer({
config: "/global",
config: testConfig,
filesystemLayer: failingFS,
locationServiceLayer: Layer.succeed(
Location.Service,
Expand Down Expand Up @@ -183,7 +185,7 @@ describe("InstructionDiscovery", () => {
Effect.flatMap((service) => service.load()),
Effect.provide(
instructionLayer({
config: "/global",
config: testConfig,
filesystemLayer: racingFS,
locationServiceLayer: Layer.succeed(
Location.Service,
Expand Down Expand Up @@ -222,7 +224,7 @@ describe("InstructionDiscovery", () => {
Effect.flatMap((service) => service.load()),
Effect.provide(
instructionLayer({
config: "/global",
config: testConfig,
filesystemLayer: observingFS,
locationServiceLayer: Layer.succeed(
Location.Service,
Expand Down Expand Up @@ -250,7 +252,7 @@ describe("InstructionDiscovery", () => {
Effect.flatMap((service) => service.load()),
Effect.provide(
instructionLayer({
config: "/global",
config: testConfig,
project: false,
filesystemLayer: Layer.effect(
FSUtil.Service,
Expand All @@ -277,7 +279,7 @@ describe("InstructionDiscovery", () => {
Effect.flatMap((service) => service.load()),
Effect.provide(
instructionLayer({
config: "/global",
config: testConfig,
filesystemLayer: Layer.effect(
FSUtil.Service,
FSUtil.Service.pipe(
Expand Down
6 changes: 4 additions & 2 deletions packages/core/test/instructions/builtins.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { describe, expect } from "bun:test"
import os from "os"
import { Effect, Layer } from "effect"
import * as TestClock from "effect/testing/TestClock"
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
Expand All @@ -16,6 +17,7 @@ const directory = AbsolutePath.make(FSUtil.resolve("/repo/packages/core"))
const projectDirectory = AbsolutePath.make(FSUtil.resolve("/repo"))
const timestamp = Date.parse("2026-06-03T12:00:00.000Z")
const sessionID = SessionSchema.ID.make("ses_builtin_test")
const temporary = os.tmpdir()
const localDate = (time: number) => new Date(time).toDateString()
const locationLayer = Layer.succeed(
Location.Service,
Expand All @@ -29,7 +31,7 @@ const locationLayer = Layer.succeed(
const it = testEffect(
AppNodeBuilder.build(InstructionBuiltIns.node, [
[Location.node, locationLayer],
[Global.node, Global.layerWith({ config: "/global", tmp: "/temporary" })],
[Global.node, Global.layerWith({ config: temporary, tmp: temporary })],
]),
)

Expand All @@ -49,7 +51,7 @@ describe("InstructionBuiltIns", () => {
` Workspace root folder: ${projectDirectory}`,
" Is directory a git repo: yes",
` Platform: ${process.platform}`,
" Use /temporary for temporary work outside the workspace; it already exists and is pre-approved for external directory access.",
` Use ${temporary} for temporary work outside the workspace; it already exists and is pre-approved for external directory access.`,
"</env>",
"",
`Today's date: ${localDate(timestamp)}`,
Expand Down
40 changes: 20 additions & 20 deletions packages/util/src/global.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import path from "path"
import fs from "fs/promises"
import fs from "fs"
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
import os from "os"
import { Context, Effect, Layer } from "effect"
Expand All @@ -13,8 +13,6 @@ const config = path.join(xdgConfig!, app)
const state = path.join(xdgState!, app)
const tmp = path.join(os.tmpdir(), app)

await fs.mkdir(tmp, { recursive: true })

const paths = {
get home() {
return process.env.OPENCODE_TEST_HOME ?? os.homedir()
Expand All @@ -26,22 +24,13 @@ const paths = {
cache,
config,
state,
tmp: await fs.realpath(tmp),
tmp,
}

export const Path = paths

Flock.setGlobal({ state })

await Promise.all([
fs.mkdir(Path.data, { recursive: true }),
fs.mkdir(Path.config, { recursive: true }),
fs.mkdir(Path.state, { recursive: true }),
fs.mkdir(Path.log, { recursive: true }),
fs.mkdir(Path.bin, { recursive: true }),
fs.mkdir(Path.repos, { recursive: true }),
])

export class Service extends Context.Service<Service, Interface>()("@opencode/Global") {}

export interface Interface {
Expand All @@ -57,31 +46,42 @@ export interface Interface {
}

export function make(input: Partial<Interface> = {}): Interface {
// The acquired service canonicalizes default tmp; use it instead of Path.tmp for path comparisons.
return {
home: Path.home,
data: Path.data,
cache: Path.cache,
config: Path.config,
state: Path.state,
tmp: Path.tmp,
tmp: input.tmp ?? Path.tmp,
bin: Path.bin,
log: Path.log,
repos: Path.repos,
...input,
}
}

const acquire = (input: Partial<Interface>) =>
Effect.gen(function* () {
const service = Service.of(make(input))
yield* Effect.promise(() =>
Promise.all(
[service.data, service.config, service.state, service.log, service.bin, service.repos, service.tmp].map(
(directory) => fs.promises.mkdir(directory, { recursive: true }),
),
),
)
const canonicalTmp = yield* Effect.promise(() => fs.promises.realpath(service.tmp))
return Service.of({ ...service, tmp: input.tmp ?? canonicalTmp })
})

const layer = Layer.effect(
Service,
Effect.sync(() => Service.of(make({ config: process.env.OPENCODE_CONFIG_DIR ?? Path.config }))),
Effect.suspend(() => acquire({ config: process.env.OPENCODE_CONFIG_DIR ?? Path.config })),
)

export const node = makeGlobalNode({ service: Service, layer: layer, deps: [] })

export const layerWith = (input: Partial<Interface>) =>
Layer.effect(
Service,
Effect.sync(() => Service.of(make(input))),
)
export const layerWith = (input: Partial<Interface>) => Layer.effect(Service, acquire(input))

export * as Global from "./global.js"
8 changes: 6 additions & 2 deletions packages/util/src/observability/logging.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Formatter, Logger, type LogLevel } from "effect"
import { Effect, FileSystem, Formatter, Logger, type LogLevel } from "effect"
import path from "path"
import { Global } from "../global.js"
import { runID } from "./shared.js"
Expand Down Expand Up @@ -53,7 +53,11 @@ export function file(local = true, channel = "local") {

export function fileLogger(target = file(), id: string = runID) {
// Do not set batchWindow to 0; it causes high idle CPU usage.
return Logger.toFile(formatter(id), target, { flag: "a" })
return Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem
yield* fs.makeDirectory(path.dirname(target), { recursive: true })
return yield* Logger.toFile(formatter(id), target, { flag: "a" })
})
}

const stderrLogger = Logger.make((options) => process.stderr.write(formatter().log(options) + "\n"))
Expand Down
93 changes: 93 additions & 0 deletions packages/util/test/global.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { describe, expect, test } from "bun:test"
import fs from "fs"
import os from "os"
import path from "path"
import { pathToFileURL } from "url"
import { Context, Effect, Layer } from "effect"
import { Global } from "../src/global.js"

describe("global", () => {
test("importing the module does not create directories", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-global-import-"))
const directories = ["data", "cache", "config", "state", "tmp"].map((directory) => path.join(root, directory))
const module = pathToFileURL(path.join(import.meta.dir, "../src/global.ts")).href
const result = Bun.spawnSync({
cmd: [process.execPath, "-e", `const { Global } = await import(${JSON.stringify(module)}); void Global.Path.tmp`],
env: {
...process.env,
XDG_DATA_HOME: directories[0],
XDG_CACHE_HOME: directories[1],
XDG_CONFIG_HOME: directories[2],
XDG_STATE_HOME: directories[3],
TMPDIR: directories[4],
},
stderr: "pipe",
})

expect(result.exitCode, result.stderr.toString()).toBe(0)
directories.forEach((directory) => expect(fs.existsSync(path.join(directory, "opencode"))).toBe(false))
fs.rmSync(root, { recursive: true, force: true })
})

test("building layerWith creates service directories and preserves an explicit tmp", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-global-layer-"))
const directories = {
data: path.join(root, "data"),
config: path.join(root, "config"),
state: path.join(root, "state"),
log: path.join(root, "log"),
bin: path.join(root, "bin"),
repos: path.join(root, "repos"),
tmp: path.join(root, "nested", "..", "tmp"),
}

const context = await Effect.runPromise(Effect.scoped(Layer.build(Global.layerWith(directories))))

Object.values(directories).forEach((directory) => expect(fs.statSync(directory).isDirectory()).toBe(true))
expect(Context.get(context, Global.Service).tmp).toBe(directories.tmp)
fs.rmSync(root, { recursive: true, force: true })
})

test("building a layer with default tmp creates and canonicalizes it", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "opencode-global-layer-"))
const directories = ["data", "cache", "config", "state", "tmp"].map((directory) => path.join(root, directory))
const module = pathToFileURL(path.join(import.meta.dir, "../src/global.ts")).href
const result = Bun.spawnSync({
cmd: [
process.execPath,
"-e",
`
import { Context, Effect, Layer } from "effect"
const { Global } = await import(${JSON.stringify(module)})
const context = await Effect.runPromise(Effect.scoped(Layer.build(Global.layerWith({}))))
process.stdout.write(Context.get(context, Global.Service).tmp)
`,
],
cwd: path.join(import.meta.dir, ".."),
env: {
...process.env,
XDG_DATA_HOME: directories[0],
XDG_CACHE_HOME: directories[1],
XDG_CONFIG_HOME: directories[2],
XDG_STATE_HOME: directories[3],
TMPDIR: directories[4],
},
stdout: "pipe",
stderr: "pipe",
})

expect(result.exitCode, result.stderr.toString()).toBe(0)
expect(result.stdout.toString()).toBe(fs.realpathSync(path.join(directories[4], "opencode")))
const created = [
path.join(directories[0], "opencode"),
path.join(directories[1], "opencode", "bin"),
path.join(directories[2], "opencode"),
path.join(directories[3], "opencode"),
path.join(directories[0], "opencode", "log"),
path.join(directories[0], "opencode", "repos"),
path.join(directories[4], "opencode"),
]
created.forEach((directory) => expect(fs.statSync(directory).isDirectory()).toBe(true))
fs.rmSync(root, { recursive: true, force: true })
})
})
Loading