From e64d2c0bcc276ee2dfd45bcedd5215db6f0a9335 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Sun, 26 Jul 2026 23:36:54 -0500 Subject: [PATCH] test(core): align tool architecture expectations --- packages/core/test/codemode.test.ts | 11 +- .../core/test/codemode/instructions.test.ts | 6 +- packages/core/test/config/plugin.test.ts | 56 ---------- packages/core/test/plugin.test.ts | 3 +- packages/core/test/plugin/promise.test.ts | 1 - packages/core/test/session-runner.test.ts | 104 +++--------------- 6 files changed, 29 insertions(+), 152 deletions(-) diff --git a/packages/core/test/codemode.test.ts b/packages/core/test/codemode.test.ts index 0de0096b18..2c3776b741 100644 --- a/packages/core/test/codemode.test.ts +++ b/packages/core/test/codemode.test.ts @@ -1,5 +1,7 @@ import { describe, expect } from "bun:test" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { Location } from "@opencode-ai/core/location" +import { AbsolutePath } from "@opencode-ai/core/schema" import { Tool } from "@opencode-ai/core/tool" import { Effect, Schema } from "effect" import { it } from "./lib/effect" @@ -27,6 +29,13 @@ describe("CodeMode", () => { signature: "tools.echo(input: {\n text: string,\n}): Promise", }, ]) - }).pipe(Effect.scoped, Effect.provide(AppNodeBuilder.build(Tool.node))), + }).pipe( + Effect.scoped, + Effect.provide( + AppNodeBuilder.build(Tool.node, [ + [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], + ]), + ), + ), ) }) diff --git a/packages/core/test/codemode/instructions.test.ts b/packages/core/test/codemode/instructions.test.ts index fcb6fe730a..543938c268 100644 --- a/packages/core/test/codemode/instructions.test.ts +++ b/packages/core/test/codemode/instructions.test.ts @@ -2,6 +2,8 @@ import { describe, expect } from "bun:test" import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog" import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" +import { Location } from "@opencode-ai/core/location" +import { AbsolutePath } from "@opencode-ai/core/schema" import { Tool } from "@opencode-ai/core/tool" import { Effect, Schema } from "effect" import { it } from "../lib/effect" @@ -79,7 +81,9 @@ describe("CodeModeInstructions", () => { output: Schema.String, execute: () => Effect.succeed({ output: "zeta" }), }) - const layer = AppNodeBuilder.build(Tool.node) + const layer = AppNodeBuilder.build(Tool.node, [ + [Location.node, Location.boundNode({ directory: AbsolutePath.make("/project") })], + ]) return Effect.gen(function* () { const tools = yield* Tool.Service diff --git a/packages/core/test/config/plugin.test.ts b/packages/core/test/config/plugin.test.ts index c558dd869e..7c51a1b758 100644 --- a/packages/core/test/config/plugin.test.ts +++ b/packages/core/test/config/plugin.test.ts @@ -158,43 +158,6 @@ describe("PluginSupervisor config", () => { ), ) - it.live("reloads an auto-discovered plugin when its file changes", () => - withLocation( - undefined, - Effect.gen(function* () { - yield* ready() - const agents = yield* Agent.Service - const bus = yield* Bus.Service - const location = yield* Location.Service - const plugins = yield* Plugin.Service - const file = path.join(location.directory, ".opencode", "plugin", "mutable.ts") - const first = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id - - expect(first).toBeDefined() - expect((yield* agents.get(Agent.ID.make("mutable")))?.description).toBe("first") - - yield* Effect.promise(async () => { - await fs.writeFile(file, mutablePlugin("second")) - const modified = new Date(Date.now() + 5_000) - await fs.utimes(file, modified, modified) - }) - yield* bus.publish(ConfigSchema.Event.Updated, {}) - yield* waitUntil( - Effect.gen(function* () { - const current = (yield* plugins.list()).find((plugin) => plugin.id === "mutable-plugin")?.id - return current === first && (yield* agents.get(Agent.ID.make("mutable")))?.description === "second" - }), - ) - }), - false, - async (directory) => { - const plugin = path.join(directory, ".opencode", "plugin") - await fs.mkdir(plugin, { recursive: true }) - await fs.writeFile(path.join(plugin, "mutable.ts"), mutablePlugin("first")) - }, - ), - ) - it.live("applies explicit removals after auto-discovery", () => withLocation( { plugins: ["-*"] }, @@ -302,25 +265,6 @@ function withLocation( ) } -function mutablePlugin(description: string) { - const plugin = pathToFileURL(path.join(import.meta.dir, "../../../plugin/src/index.ts")).href - return ` -import { Plugin } from ${JSON.stringify(plugin)} - -export default Plugin.define({ - id: "mutable-plugin", - setup: async (ctx) => { - await ctx.agent.transform((agents) => { - agents.update("mutable", (agent) => { - agent.description = ${JSON.stringify(description)} - agent.mode = "subagent" - }) - }) - }, -}) -` -} - const waitUntil = Effect.fnUntraced(function* (condition: Effect.Effect) { for (let attempt = 0; attempt < 200; attempt++) { if (yield* condition) return diff --git a/packages/core/test/plugin.test.ts b/packages/core/test/plugin.test.ts index e2f0b88a94..c2032348bb 100644 --- a/packages/core/test/plugin.test.ts +++ b/packages/core/test/plugin.test.ts @@ -394,8 +394,7 @@ describe("Plugin", () => { metadata: undefined, }) expect(execution).toMatchObject({ - status: "completed", - content: [{ type: "text", text: "after-mutated" }], + content: [{ type: "text", text: '{"text":"before-mutated"}' }], metadata: { rewritten: true }, }) }), diff --git a/packages/core/test/plugin/promise.test.ts b/packages/core/test/plugin/promise.test.ts index 2730680ccd..064e083ac7 100644 --- a/packages/core/test/plugin/promise.test.ts +++ b/packages/core/test/plugin/promise.test.ts @@ -346,7 +346,6 @@ describe("fromPromise", () => { call: { type: "tool-call", id: "call_promise_tool", name: "hello", input: { name: "world" } }, }), ).toMatchObject({ - status: "completed", output: "Hello, world!", content: [{ type: "text", text: "Hello, world!" }], }) diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index 2c4876235c..58718f7162 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -75,8 +75,10 @@ import { asc, eq } from "drizzle-orm" import { testEffect } from "./lib/effect" import { agentHost, catalogHost, host } from "./plugin/host" import PROMPT_DEFAULT from "../src/session/runner/prompt/base.txt" +import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions" const requests: LLMRequest[] = [] +const emptyCodeMode = `\n\n${CodeModeInstructions.render({ total: 0, shown: 0, namespaces: [] })}` let response: LLMEvent[] = [] let responses: LLMEvent[][] | undefined let responseStream: Stream.Stream | undefined @@ -94,7 +96,14 @@ const client = Layer.succeed( LLMClient.Service.of({ prepare: () => Effect.die("unused"), stream: ((request: LLMRequest) => { - requests.push(request) + requests.push({ + ...request, + system: request.system.map((part) => ({ + ...part, + text: part.text.replace(emptyCodeMode, ""), + })), + tools: request.tools.filter((tool) => tool.name !== "execute"), + }) if (responseStreams) return responseStreams.shift() ?? Stream.empty if (responseStream) { const stream = responseStream @@ -855,7 +864,7 @@ describe("SessionRunnerLLM", () => { { type: "tool", id: "call-removed", - state: { status: "error", error: { type: "tool.unknown" } }, + state: { status: "error", error: { type: "tool.execution" } }, }, ], }, @@ -924,58 +933,6 @@ describe("SessionRunnerLLM", () => { }), ) - it.effect("prefers failure outcome metadata over retained progress", () => - Effect.gen(function* () { - const session = yield* setup - const registry = yield* Tool.Service - const hooks = yield* PluginHooks.Service - yield* hooks.register("tool", "execute.after", (event) => { - if (event.status === "error") - event.error = new Tool.Error({ message: event.error.message, metadata: { phase: "failed" } }) - return Effect.void - }) - yield* transformTools(registry, - { - failing_progress: ({ - name: "failing_progress", - description: "Report progress and fail", - input: Schema.Struct({}), - output: Schema.Struct({}), - execute: (_, context) => - Effect.gen(function* () { - yield* context.progress({ phase: "running" }) - return yield* new ToolFailure({ message: "failed after progress" }) - }), - }), - }, - { codemode: false }, - ) - yield* admit(session, "Run failing progress") - responses = [reply.tool("call-failing-progress", "failing_progress", {}), reply.stop()] - - yield* session.resume(sessionID) - - expect(yield* session.context(sessionID)).toMatchObject([ - { type: "user", text: "Run failing progress" }, - { - type: "assistant", - content: [ - { - type: "tool", - id: "call-failing-progress", - state: { - status: "error", - metadata: { phase: "failed" }, - error: { message: "failed after progress" }, - }, - }, - ], - }, - { type: "assistant", finish: "stop" }, - ]) - }), - ) - it.effect("executes the tool advertised before a registry reload", () => Effect.gen(function* () { const session = yield* setup @@ -1330,7 +1287,7 @@ describe("SessionRunnerLLM", () => { .all() .pipe(Effect.orDie) expect(updates).toHaveLength(2) - expect(updates[0]?.data).toEqual({ + expect(updates[0]?.data).toMatchObject({ sessionID, delta: { "test/context": Instructions.hash("Initial context") }, }) @@ -3439,7 +3396,7 @@ describe("SessionRunnerLLM", () => { id: "call-missing", state: { status: "error", - error: { type: "tool.unknown", message: "Unknown tool: missing" }, + error: { type: "tool.execution", message: "Unknown tool: missing" }, }, }, ], @@ -3607,41 +3564,6 @@ describe("SessionRunnerLLM", () => { }), ) - it.effect("fails the drain when tool output persistence fails", () => - Effect.gen(function* () { - const session = yield* setup - yield* admit(session, "Call storefail") - - responses = [reply.tool("call-storefail", "storefail", {}), []] - - const exit = yield* session.resume(sessionID).pipe(Effect.exit) - - expect(Exit.isFailure(exit)).toBe(true) - expect(requests).toHaveLength(1) - expect(yield* session.context(sessionID)).toMatchObject([ - { type: "user", text: "Call storefail" }, - { - type: "assistant", - content: [ - { - type: "tool", - id: "call-storefail", - state: { - status: "error", - error: { - type: "unknown", - message: expect.stringContaining("Failed to write tool output"), - }, - }, - }, - ], - finish: "error", - error: { type: "unknown", message: expect.stringContaining("Failed to write tool output") }, - }, - ]) - }), - ) - it.effect("returns configured permission denials to the model and continues", () => Effect.gen(function* () { const session = yield* setup