From 319b7655b7f6d7b32f1d6a2de4de78617195dcc9 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Sat, 11 Apr 2026 21:20:12 -0400 Subject: [PATCH] refactor(tool): destroy Truncate facade, effectify Tool.define (#22093) --- packages/opencode/src/tool/tool.ts | 20 +- packages/opencode/src/tool/truncate.ts | 7 - .../opencode/test/tool/apply_patch.test.ts | 4 +- packages/opencode/test/tool/bash.test.ts | 9 +- packages/opencode/test/tool/edit.test.ts | 4 +- packages/opencode/test/tool/grep.test.ts | 4 +- packages/opencode/test/tool/question.test.ts | 4 +- packages/opencode/test/tool/read.test.ts | 2 + packages/opencode/test/tool/skill.test.ts | 4 +- packages/opencode/test/tool/task.test.ts | 2 + .../opencode/test/tool/tool-define.test.ts | 12 +- .../opencode/test/tool/truncation.test.ts | 211 ++++++++++-------- packages/opencode/test/tool/webfetch.test.ts | 24 +- packages/opencode/test/tool/write.test.ts | 4 + 14 files changed, 186 insertions(+), 125 deletions(-) diff --git a/packages/opencode/src/tool/tool.ts b/packages/opencode/src/tool/tool.ts index 3009f4cd27..49dd2b0605 100644 --- a/packages/opencode/src/tool/tool.ts +++ b/packages/opencode/src/tool/tool.ts @@ -70,7 +70,12 @@ export namespace Tool { ? Def : never - function wrap(id: string, init: Init) { + function wrap( + id: string, + init: Init, + truncate: Truncate.Interface, + agents: Agent.Interface, + ) { return () => Effect.gen(function* () { const toolInfo = init instanceof Function ? { ...(yield* init()) } : { ...init } @@ -93,8 +98,8 @@ export namespace Tool { if (result.metadata.truncated !== undefined) { return result } - const agent = yield* Effect.promise(() => Agent.get(ctx.agent)) - const truncated = yield* Effect.promise(() => Truncate.output(result.output, {}, agent)) + const agent = yield* agents.get(ctx.agent) + const truncated = yield* truncate.output(result.output, {}, agent) return { ...result, output: truncated.content, @@ -112,9 +117,14 @@ export namespace Tool { export function define( id: ID, init: Effect.Effect, never, R>, - ): Effect.Effect, never, R> & { id: ID } { + ): Effect.Effect, never, R | Truncate.Service | Agent.Service> & { id: ID } { return Object.assign( - Effect.map(init, (init) => ({ id, init: wrap(id, init) })), + Effect.gen(function* () { + const resolved = yield* init + const truncate = yield* Truncate.Service + const agents = yield* Agent.Service + return { id, init: wrap(id, resolved, truncate, agents) } + }), { id }, ) } diff --git a/packages/opencode/src/tool/truncate.ts b/packages/opencode/src/tool/truncate.ts index 716929cc64..fa9e0bcabe 100644 --- a/packages/opencode/src/tool/truncate.ts +++ b/packages/opencode/src/tool/truncate.ts @@ -2,7 +2,6 @@ import { NodePath } from "@effect/platform-node" import { Cause, Duration, Effect, Layer, Schedule, Context } from "effect" import path from "path" import type { Agent } from "../agent/agent" -import { makeRuntime } from "@/effect/run-service" import { AppFileSystem } from "@/filesystem" import { evaluate } from "@/permission/evaluate" import { Identifier } from "../id/id" @@ -135,10 +134,4 @@ export namespace Truncate { ) export const defaultLayer = layer.pipe(Layer.provide(AppFileSystem.defaultLayer), Layer.provide(NodePath.layer)) - - const { runPromise } = makeRuntime(Service, defaultLayer) - - export async function output(text: string, options: Options = {}, agent?: Agent.Info): Promise { - return runPromise((s) => s.output(text, options, agent)) - } } diff --git a/packages/opencode/test/tool/apply_patch.test.ts b/packages/opencode/test/tool/apply_patch.test.ts index 4efa12f2fc..d65b1126c4 100644 --- a/packages/opencode/test/tool/apply_patch.test.ts +++ b/packages/opencode/test/tool/apply_patch.test.ts @@ -7,12 +7,14 @@ import { Instance } from "../../src/project/instance" import { LSP } from "../../src/lsp" import { AppFileSystem } from "../../src/filesystem" import { Format } from "../../src/format" +import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" +import { Truncate } from "../../src/tool/truncate" import { tmpdir } from "../fixture/fixture" import { SessionID, MessageID } from "../../src/session/schema" const runtime = ManagedRuntime.make( - Layer.mergeAll(LSP.defaultLayer, AppFileSystem.defaultLayer, Format.defaultLayer, Bus.layer), + Layer.mergeAll(LSP.defaultLayer, AppFileSystem.defaultLayer, Format.defaultLayer, Bus.layer, Truncate.defaultLayer, Agent.defaultLayer), ) const baseCtx = { diff --git a/packages/opencode/test/tool/bash.test.ts b/packages/opencode/test/tool/bash.test.ts index e9a6ae38cf..839c066c6c 100644 --- a/packages/opencode/test/tool/bash.test.ts +++ b/packages/opencode/test/tool/bash.test.ts @@ -8,6 +8,7 @@ import { Instance } from "../../src/project/instance" import { Filesystem } from "../../src/util/filesystem" import { tmpdir } from "../fixture/fixture" import type { Permission } from "../../src/permission" +import { Agent } from "../../src/agent/agent" import { Truncate } from "../../src/tool/truncate" import { SessionID, MessageID } from "../../src/session/schema" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" @@ -15,7 +16,13 @@ import { AppFileSystem } from "../../src/filesystem" import { Plugin } from "../../src/plugin" const runtime = ManagedRuntime.make( - Layer.mergeAll(CrossSpawnSpawner.defaultLayer, AppFileSystem.defaultLayer, Plugin.defaultLayer), + Layer.mergeAll( + CrossSpawnSpawner.defaultLayer, + AppFileSystem.defaultLayer, + Plugin.defaultLayer, + Truncate.defaultLayer, + Agent.defaultLayer, + ), ) function initBash() { diff --git a/packages/opencode/test/tool/edit.test.ts b/packages/opencode/test/tool/edit.test.ts index 0695b54bac..c19c22d2c5 100644 --- a/packages/opencode/test/tool/edit.test.ts +++ b/packages/opencode/test/tool/edit.test.ts @@ -9,8 +9,10 @@ import { FileTime } from "../../src/file/time" import { LSP } from "../../src/lsp" import { AppFileSystem } from "../../src/filesystem" import { Format } from "../../src/format" +import { Agent } from "../../src/agent/agent" import { Bus } from "../../src/bus" import { BusEvent } from "../../src/bus/bus-event" +import { Truncate } from "../../src/tool/truncate" import { SessionID, MessageID } from "../../src/session/schema" const ctx = { @@ -34,7 +36,7 @@ async function touch(file: string, time: number) { } const runtime = ManagedRuntime.make( - Layer.mergeAll(LSP.defaultLayer, FileTime.defaultLayer, AppFileSystem.defaultLayer, Format.defaultLayer, Bus.layer), + Layer.mergeAll(LSP.defaultLayer, FileTime.defaultLayer, AppFileSystem.defaultLayer, Format.defaultLayer, Bus.layer, Truncate.defaultLayer, Agent.defaultLayer), ) afterAll(async () => { diff --git a/packages/opencode/test/tool/grep.test.ts b/packages/opencode/test/tool/grep.test.ts index 4078c9ce6a..926b26003c 100644 --- a/packages/opencode/test/tool/grep.test.ts +++ b/packages/opencode/test/tool/grep.test.ts @@ -6,8 +6,10 @@ import { Instance } from "../../src/project/instance" import { tmpdir } from "../fixture/fixture" import { SessionID, MessageID } from "../../src/session/schema" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { Truncate } from "../../src/tool/truncate" +import { Agent } from "../../src/agent/agent" -const runtime = ManagedRuntime.make(Layer.mergeAll(CrossSpawnSpawner.defaultLayer)) +const runtime = ManagedRuntime.make(Layer.mergeAll(CrossSpawnSpawner.defaultLayer, Truncate.defaultLayer, Agent.defaultLayer)) function initGrep() { return runtime.runPromise(GrepTool.pipe(Effect.flatMap((info) => info.init()))) diff --git a/packages/opencode/test/tool/question.test.ts b/packages/opencode/test/tool/question.test.ts index f651f019ef..4b94552368 100644 --- a/packages/opencode/test/tool/question.test.ts +++ b/packages/opencode/test/tool/question.test.ts @@ -4,7 +4,9 @@ import { Tool } from "../../src/tool/tool" import { QuestionTool } from "../../src/tool/question" import { Question } from "../../src/question" import { SessionID, MessageID } from "../../src/session/schema" +import { Agent } from "../../src/agent/agent" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" +import { Truncate } from "../../src/tool/truncate" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" @@ -19,7 +21,7 @@ const ctx = { ask: () => Effect.void, } -const it = testEffect(Layer.mergeAll(Question.defaultLayer, CrossSpawnSpawner.defaultLayer)) +const it = testEffect(Layer.mergeAll(Question.defaultLayer, CrossSpawnSpawner.defaultLayer, Truncate.defaultLayer, Agent.defaultLayer)) const pending = Effect.fn("QuestionToolTest.pending")(function* (question: Question.Interface) { for (;;) { diff --git a/packages/opencode/test/tool/read.test.ts b/packages/opencode/test/tool/read.test.ts index c41cefda37..2064193d5b 100644 --- a/packages/opencode/test/tool/read.test.ts +++ b/packages/opencode/test/tool/read.test.ts @@ -11,6 +11,7 @@ import { Instance } from "../../src/project/instance" import { SessionID, MessageID } from "../../src/session/schema" import { Instruction } from "../../src/session/instruction" import { ReadTool } from "../../src/tool/read" +import { Truncate } from "../../src/tool/truncate" import { Tool } from "../../src/tool/tool" import { Filesystem } from "../../src/util/filesystem" import { provideInstance, tmpdirScoped } from "../fixture/fixture" @@ -41,6 +42,7 @@ const it = testEffect( FileTime.defaultLayer, Instruction.defaultLayer, LSP.defaultLayer, + Truncate.defaultLayer, ), ) diff --git a/packages/opencode/test/tool/skill.test.ts b/packages/opencode/test/tool/skill.test.ts index 47a8321efc..85af7f3789 100644 --- a/packages/opencode/test/tool/skill.test.ts +++ b/packages/opencode/test/tool/skill.test.ts @@ -1,6 +1,8 @@ import { Effect, Layer, ManagedRuntime } from "effect" +import { Agent } from "../../src/agent/agent" import { Skill } from "../../src/skill" import { Ripgrep } from "../../src/file/ripgrep" +import { Truncate } from "../../src/tool/truncate" import { afterEach, describe, expect, test } from "bun:test" import path from "path" import { pathToFileURL } from "url" @@ -150,7 +152,7 @@ Use this skill. await Instance.provide({ directory: tmp.path, fn: async () => { - const runtime = ManagedRuntime.make(Layer.mergeAll(Skill.defaultLayer, Ripgrep.defaultLayer)) + const runtime = ManagedRuntime.make(Layer.mergeAll(Skill.defaultLayer, Ripgrep.defaultLayer, Truncate.defaultLayer, Agent.defaultLayer)) const info = await runtime.runPromise(SkillTool) const tool = await runtime.runPromise(info.init()) const requests: Array> = [] diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index e2ae52bf05..436c46490b 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -10,6 +10,7 @@ import type { SessionPrompt } from "../../src/session/prompt" import { MessageID, PartID } from "../../src/session/schema" import { ModelID, ProviderID } from "../../src/provider/schema" import { TaskTool, type TaskPromptOps } from "../../src/tool/task" +import { Truncate } from "../../src/tool/truncate" import { ToolRegistry } from "../../src/tool/registry" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" @@ -29,6 +30,7 @@ const it = testEffect( Config.defaultLayer, CrossSpawnSpawner.defaultLayer, Session.defaultLayer, + Truncate.defaultLayer, ToolRegistry.defaultLayer, ), ) diff --git a/packages/opencode/test/tool/tool-define.test.ts b/packages/opencode/test/tool/tool-define.test.ts index 425c83ffdd..b8003e475d 100644 --- a/packages/opencode/test/tool/tool-define.test.ts +++ b/packages/opencode/test/tool/tool-define.test.ts @@ -1,7 +1,11 @@ import { describe, test, expect } from "bun:test" -import { Effect } from "effect" +import { Effect, Layer, ManagedRuntime } from "effect" import z from "zod" +import { Agent } from "../../src/agent/agent" import { Tool } from "../../src/tool/tool" +import { Truncate } from "../../src/tool/truncate" + +const runtime = ManagedRuntime.make(Layer.mergeAll(Truncate.defaultLayer, Agent.defaultLayer)) const params = z.object({ input: z.string() }) @@ -21,7 +25,7 @@ describe("Tool.define", () => { const original = makeTool("test") const originalExecute = original.execute - const info = await Effect.runPromise(Tool.define("test-tool", Effect.succeed(original))) + const info = await runtime.runPromise(Tool.define("test-tool", Effect.succeed(original))) await Effect.runPromise(info.init()) await Effect.runPromise(info.init()) @@ -31,7 +35,7 @@ describe("Tool.define", () => { }) test("effect-defined tool returns fresh objects and is unaffected", async () => { - const info = await Effect.runPromise( + const info = await runtime.runPromise( Tool.define( "test-fn-tool", Effect.succeed(() => Effect.succeed(makeTool("test"))), @@ -45,7 +49,7 @@ describe("Tool.define", () => { }) test("object-defined tool returns distinct objects per init() call", async () => { - const info = await Effect.runPromise(Tool.define("test-copy", Effect.succeed(makeTool("test")))) + const info = await runtime.runPromise(Tool.define("test-copy", Effect.succeed(makeTool("test")))) const first = await Effect.runPromise(info.init()) const second = await Effect.runPromise(info.init()) diff --git a/packages/opencode/test/tool/truncation.test.ts b/packages/opencode/test/tool/truncation.test.ts index 9ec5b78400..493cd9d7e1 100644 --- a/packages/opencode/test/tool/truncation.test.ts +++ b/packages/opencode/test/tool/truncation.test.ts @@ -1,7 +1,7 @@ import { describe, test, expect } from "bun:test" import { NodeFileSystem } from "@effect/platform-node" import { Effect, FileSystem, Layer } from "effect" -import { Truncate, Truncate as TruncateSvc } from "../../src/tool/truncate" +import { Truncate } from "../../src/tool/truncate" import { Identifier } from "../../src/id/id" import { Process } from "../../src/util/process" import { Filesystem } from "../../src/util/filesystem" @@ -12,120 +12,155 @@ import { writeFileStringScoped } from "../lib/filesystem" const FIXTURES_DIR = path.join(import.meta.dir, "fixtures") const ROOT = path.resolve(import.meta.dir, "..", "..") +const it = testEffect(Layer.mergeAll(Truncate.defaultLayer, NodeFileSystem.layer)) + describe("Truncate", () => { describe("output", () => { - test("truncates large json file by bytes", async () => { - const content = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json")) - const result = await Truncate.output(content) + it.live("truncates large json file by bytes", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const content = yield* Effect.promise(() => Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json"))) + const result = yield* svc.output(content) - expect(result.truncated).toBe(true) - expect(result.content).toContain("truncated...") - if (result.truncated) expect(result.outputPath).toBeDefined() - }) + expect(result.truncated).toBe(true) + expect(result.content).toContain("truncated...") + if (result.truncated) expect(result.outputPath).toBeDefined() + }), + ) - test("returns content unchanged when under limits", async () => { - const content = "line1\nline2\nline3" - const result = await Truncate.output(content) + it.live("returns content unchanged when under limits", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const content = "line1\nline2\nline3" + const result = yield* svc.output(content) - expect(result.truncated).toBe(false) - expect(result.content).toBe(content) - }) + expect(result.truncated).toBe(false) + expect(result.content).toBe(content) + }), + ) - test("truncates by line count", async () => { - const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") - const result = await Truncate.output(lines, { maxLines: 10 }) + it.live("truncates by line count", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") + const result = yield* svc.output(lines, { maxLines: 10 }) - expect(result.truncated).toBe(true) - expect(result.content).toContain("...90 lines truncated...") - }) + expect(result.truncated).toBe(true) + expect(result.content).toContain("...90 lines truncated...") + }), + ) - test("truncates by byte count", async () => { - const content = "a".repeat(1000) - const result = await Truncate.output(content, { maxBytes: 100 }) + it.live("truncates by byte count", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const content = "a".repeat(1000) + const result = yield* svc.output(content, { maxBytes: 100 }) - expect(result.truncated).toBe(true) - expect(result.content).toContain("truncated...") - }) + expect(result.truncated).toBe(true) + expect(result.content).toContain("truncated...") + }), + ) - test("truncates from head by default", async () => { - const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n") - const result = await Truncate.output(lines, { maxLines: 3 }) + it.live("truncates from head by default", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n") + const result = yield* svc.output(lines, { maxLines: 3 }) - expect(result.truncated).toBe(true) - expect(result.content).toContain("line0") - expect(result.content).toContain("line1") - expect(result.content).toContain("line2") - expect(result.content).not.toContain("line9") - }) + expect(result.truncated).toBe(true) + expect(result.content).toContain("line0") + expect(result.content).toContain("line1") + expect(result.content).toContain("line2") + expect(result.content).not.toContain("line9") + }), + ) - test("truncates from tail when direction is tail", async () => { - const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n") - const result = await Truncate.output(lines, { maxLines: 3, direction: "tail" }) + it.live("truncates from tail when direction is tail", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const lines = Array.from({ length: 10 }, (_, i) => `line${i}`).join("\n") + const result = yield* svc.output(lines, { maxLines: 3, direction: "tail" }) - expect(result.truncated).toBe(true) - expect(result.content).toContain("line7") - expect(result.content).toContain("line8") - expect(result.content).toContain("line9") - expect(result.content).not.toContain("line0") - }) + expect(result.truncated).toBe(true) + expect(result.content).toContain("line7") + expect(result.content).toContain("line8") + expect(result.content).toContain("line9") + expect(result.content).not.toContain("line0") + }), + ) test("uses default MAX_LINES and MAX_BYTES", () => { expect(Truncate.MAX_LINES).toBe(2000) expect(Truncate.MAX_BYTES).toBe(50 * 1024) }) - test("large single-line file truncates with byte message", async () => { - const content = await Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json")) - const result = await Truncate.output(content) + it.live("large single-line file truncates with byte message", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const content = yield* Effect.promise(() => Filesystem.readText(path.join(FIXTURES_DIR, "models-api.json"))) + const result = yield* svc.output(content) - expect(result.truncated).toBe(true) - expect(result.content).toContain("bytes truncated...") - expect(Buffer.byteLength(content, "utf-8")).toBeGreaterThan(Truncate.MAX_BYTES) - }) + expect(result.truncated).toBe(true) + expect(result.content).toContain("bytes truncated...") + expect(Buffer.byteLength(content, "utf-8")).toBeGreaterThan(Truncate.MAX_BYTES) + }), + ) - test("writes full output to file when truncated", async () => { - const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") - const result = await Truncate.output(lines, { maxLines: 10 }) + it.live("writes full output to file when truncated", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") + const result = yield* svc.output(lines, { maxLines: 10 }) - expect(result.truncated).toBe(true) - expect(result.content).toContain("The tool call succeeded but the output was truncated") - expect(result.content).toContain("Grep") - if (!result.truncated) throw new Error("expected truncated") - expect(result.outputPath).toBeDefined() - expect(result.outputPath).toContain("tool_") + expect(result.truncated).toBe(true) + expect(result.content).toContain("The tool call succeeded but the output was truncated") + expect(result.content).toContain("Grep") + if (!result.truncated) throw new Error("expected truncated") + expect(result.outputPath).toBeDefined() + expect(result.outputPath).toContain("tool_") - const written = await Filesystem.readText(result.outputPath!) - expect(written).toBe(lines) - }) + const written = yield* Effect.promise(() => Filesystem.readText(result.outputPath!)) + expect(written).toBe(lines) + }), + ) - test("suggests Task tool when agent has task permission", async () => { - const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") - const agent = { permission: [{ permission: "task", pattern: "*", action: "allow" as const }] } - const result = await Truncate.output(lines, { maxLines: 10 }, agent as any) + it.live("suggests Task tool when agent has task permission", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") + const agent = { permission: [{ permission: "task", pattern: "*", action: "allow" as const }] } + const result = yield* svc.output(lines, { maxLines: 10 }, agent as any) - expect(result.truncated).toBe(true) - expect(result.content).toContain("Grep") - expect(result.content).toContain("Task tool") - }) + expect(result.truncated).toBe(true) + expect(result.content).toContain("Grep") + expect(result.content).toContain("Task tool") + }), + ) - test("omits Task tool hint when agent lacks task permission", async () => { - const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") - const agent = { permission: [{ permission: "task", pattern: "*", action: "deny" as const }] } - const result = await Truncate.output(lines, { maxLines: 10 }, agent as any) + it.live("omits Task tool hint when agent lacks task permission", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const lines = Array.from({ length: 100 }, (_, i) => `line${i}`).join("\n") + const agent = { permission: [{ permission: "task", pattern: "*", action: "deny" as const }] } + const result = yield* svc.output(lines, { maxLines: 10 }, agent as any) - expect(result.truncated).toBe(true) - expect(result.content).toContain("Grep") - expect(result.content).not.toContain("Task tool") - }) + expect(result.truncated).toBe(true) + expect(result.content).toContain("Grep") + expect(result.content).not.toContain("Task tool") + }), + ) - test("does not write file when not truncated", async () => { - const content = "short content" - const result = await Truncate.output(content) + it.live("does not write file when not truncated", () => + Effect.gen(function* () { + const svc = yield* Truncate.Service + const content = "short content" + const result = yield* svc.output(content) - expect(result.truncated).toBe(false) - if (result.truncated) throw new Error("expected not truncated") - expect("outputPath" in result).toBe(false) - }) + expect(result.truncated).toBe(false) + if (result.truncated) throw new Error("expected not truncated") + expect("outputPath" in result).toBe(false) + }), + ) test("loads truncate effect in a fresh process", async () => { const out = await Process.run([process.execPath, "run", path.join(ROOT, "src", "tool", "truncate.ts")], { @@ -138,10 +173,10 @@ describe("Truncate", () => { describe("cleanup", () => { const DAY_MS = 24 * 60 * 60 * 1000 - const it = testEffect(Layer.mergeAll(TruncateSvc.defaultLayer, NodeFileSystem.layer)) it.live("deletes files older than 7 days and preserves recent files", () => Effect.gen(function* () { + const svc = yield* Truncate.Service const fs = yield* FileSystem.FileSystem yield* fs.makeDirectory(Truncate.DIR, { recursive: true }) @@ -151,7 +186,7 @@ describe("Truncate", () => { yield* writeFileStringScoped(old, "old content") yield* writeFileStringScoped(recent, "recent content") - yield* TruncateSvc.Service.use((s) => s.cleanup()) + yield* svc.cleanup() expect(yield* fs.exists(old)).toBe(false) expect(yield* fs.exists(recent)).toBe(true) diff --git a/packages/opencode/test/tool/webfetch.test.ts b/packages/opencode/test/tool/webfetch.test.ts index c6b2fd331c..7d2ff1dcab 100644 --- a/packages/opencode/test/tool/webfetch.test.ts +++ b/packages/opencode/test/tool/webfetch.test.ts @@ -1,7 +1,9 @@ import { describe, expect, test } from "bun:test" import path from "path" -import { Effect } from "effect" +import { Effect, Layer } from "effect" import { FetchHttpClient } from "effect/unstable/http" +import { Agent } from "../../src/agent/agent" +import { Truncate } from "../../src/tool/truncate" import { Instance } from "../../src/project/instance" import { WebFetchTool } from "../../src/tool/webfetch" import { SessionID, MessageID } from "../../src/session/schema" @@ -24,10 +26,11 @@ async function withFetch(fetch: (req: Request) => Response | Promise, await fn(server.url) } -function initTool() { +function exec(args: { url: string; format: "text" | "markdown" | "html" }) { return WebFetchTool.pipe( Effect.flatMap((info) => info.init()), - Effect.provide(FetchHttpClient.layer), + Effect.flatMap((tool) => tool.execute(args, ctx)), + Effect.provide(Layer.mergeAll(FetchHttpClient.layer, Truncate.defaultLayer, Agent.defaultLayer)), Effect.runPromise, ) } @@ -41,10 +44,7 @@ describe("tool.webfetch", () => { await Instance.provide({ directory: projectRoot, fn: async () => { - const webfetch = await initTool() - const result = await Effect.runPromise( - webfetch.execute({ url: new URL("/image.png", url).toString(), format: "markdown" }, ctx), - ) + const result = await exec({ url: new URL("/image.png", url).toString(), format: "markdown" }) expect(result.output).toBe("Image fetched successfully") expect(result.attachments).toBeDefined() expect(result.attachments?.length).toBe(1) @@ -72,10 +72,7 @@ describe("tool.webfetch", () => { await Instance.provide({ directory: projectRoot, fn: async () => { - const webfetch = await initTool() - const result = await Effect.runPromise( - webfetch.execute({ url: new URL("/image.svg", url).toString(), format: "html" }, ctx), - ) + const result = await exec({ url: new URL("/image.svg", url).toString(), format: "html" }) expect(result.output).toContain(" { await Instance.provide({ directory: projectRoot, fn: async () => { - const webfetch = await initTool() - const result = await Effect.runPromise( - webfetch.execute({ url: new URL("/file.txt", url).toString(), format: "text" }, ctx), - ) + const result = await exec({ url: new URL("/file.txt", url).toString(), format: "text" }) expect(result.output).toBe("hello from webfetch") expect(result.attachments).toBeUndefined() }, diff --git a/packages/opencode/test/tool/write.test.ts b/packages/opencode/test/tool/write.test.ts index 3607a9272e..f7daa1e971 100644 --- a/packages/opencode/test/tool/write.test.ts +++ b/packages/opencode/test/tool/write.test.ts @@ -9,7 +9,9 @@ import { AppFileSystem } from "../../src/filesystem" import { FileTime } from "../../src/file/time" import { Bus } from "../../src/bus" import { Format } from "../../src/format" +import { Truncate } from "../../src/tool/truncate" import { Tool } from "../../src/tool/tool" +import { Agent } from "../../src/agent/agent" import { SessionID, MessageID } from "../../src/session/schema" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { provideTmpdirInstance } from "../fixture/fixture" @@ -38,6 +40,8 @@ const it = testEffect( Bus.layer, Format.defaultLayer, CrossSpawnSpawner.defaultLayer, + Truncate.defaultLayer, + Agent.defaultLayer, ), )