diff --git a/packages/core/test/tool-shell.test.ts b/packages/core/test/tool-shell.test.ts index 5c04101d5db..ca982164814 100644 --- a/packages/core/test/tool-shell.test.ts +++ b/packages/core/test/tool-shell.test.ts @@ -3,17 +3,20 @@ import { realpathSync } from "node:fs" import os from "os" import path from "path" import { describe, expect } from "bun:test" -import { DateTime, Deferred, Duration, Effect, Fiber, Layer, Scope, Stream } from "effect" +import { Deferred, Duration, Effect, Fiber, Layer, Scope, Stream } from "effect" import { Money } from "@opencode-ai/schema/money" import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder" import { LayerNode } from "@opencode-ai/util/effect/layer-node" -import { makeGlobalNode } from "@opencode-ai/util/effect/app-node" +import { makeGlobalNode, makeLocationNode } from "@opencode-ai/util/effect/app-node" import { filesystem } from "@opencode-ai/util/effect/app-node-platform" import { Database } from "@opencode-ai/core/database/database" import { Bus } from "@opencode-ai/core/bus" +import { Config } from "@opencode-ai/core/config" +import { Environment } from "@opencode-ai/core/environment" import { FSUtil } from "@opencode-ai/util/fs-util" import { Global } from "@opencode-ai/util/global" import { Location } from "@opencode-ai/core/location" +import { LocationMutation } from "@opencode-ai/core/location-mutation" import { LocationServiceMap } from "@opencode-ai/core/location-service-map" import { Model } from "@opencode-ai/core/model" import { Provider } from "@opencode-ai/core/provider" @@ -36,7 +39,7 @@ import { Tool } from "@opencode-ai/core/tool" import { tmpdir } from "./fixture/tmpdir" import { tempGlobalLayer } from "./fixture/global" import { testEffect } from "./lib/effect" -import { toolIdentity, executeTool, toolDefinitions } from "./lib/tool" +import { toolIdentity, executeTool, registerToolPlugin, toolDefinitions } from "./lib/tool" const sessionID = Session.ID.make("ses_shell_tool_test") const sessionModel = Model.Ref.make({ id: Model.ID.make("test"), providerID: Provider.ID.make("test") }) @@ -124,27 +127,42 @@ const executionNode = makeGlobalNode({ deps: [Bus.node, SessionStore.node], }) -const layer = AppNodeBuilder.build( - LayerNode.group([ - Database.node, - Bus.node, - Job.node, - Session.node, - SessionExecution.node, - PluginRuntime.providerNode, - LocationServiceMap.node, - filesystem, - FSUtil.node, - Global.node, - ]), - [ - [SessionExecution.node, executionNode], - [Permission.node, permission], - [Global.node, tempGlobalLayer], +const shellPluginSupervisor = makeLocationNode({ + service: PluginSupervisor.Service, + layer: Layer.effect( + PluginSupervisor.Service, + registerToolPlugin(ShellTool.Plugin).pipe(Effect.as(PluginSupervisor.Service.of({ flush: Effect.void }))), + ), + deps: [ + Config.node, + Environment.node, + LocationMutation.node, + Permission.node, + PluginRuntime.node, + Shell.node, + Tool.node, ], -) +}) -const it = testEffect(layer) +const nodes = LayerNode.group([ + Database.node, + Bus.node, + Job.node, + Session.node, + SessionExecution.node, + PluginRuntime.providerNode, + LocationServiceMap.node, + filesystem, + FSUtil.node, + Global.node, +]) +const replacements = [ + [SessionExecution.node, executionNode], + [Permission.node, permission], + [Global.node, tempGlobalLayer], +] satisfies LayerNode.Replacements +const productionIt = testEffect(AppNodeBuilder.build(nodes, replacements)) +const it = testEffect(AppNodeBuilder.build(nodes, [...replacements, [PluginSupervisor.node, shellPluginSupervisor]])) const call = (input: typeof ShellTool.Input.Type, id = "call-shell") => ({ sessionID, @@ -165,9 +183,6 @@ const idleCommand = isWindows ? "Start-Sleep -Seconds 60" : "sleep 60" const timeoutOutputCommand = isWindows ? "[Console]::Out.Write('before timeout'); Start-Sleep -Seconds 60" : "printf 'before timeout'; sleep 60" -const steadyProgressCommand = isWindows - ? "[Console]::Out.Write('steady'); Start-Sleep -Milliseconds 3400" - : "printf steady; sleep 3.4" const bodyExitCommand = isWindows ? "[Console]::Out.Write('body'); Start-Sleep -Milliseconds 100; exit 7" : "printf body && exit 7" @@ -203,42 +218,49 @@ const withSession = (directory: string, body: (registry: Tool.Interface }) describe("ShellTool", () => { - it.live("registers and returns real successful output from the active Location", () => - Effect.acquireUseRelease( - Effect.promise(() => tmpdir()), - (tmp) => { - reset() - return withSession(tmp.path, (registry) => - Effect.gen(function* () { - const definitions = yield* toolDefinitions(registry) - const definition = definitions.find((tool) => tool.name === "shell") - expect(definition?.description).toStartWith("Execute a shell command and return its output.") - expect(definition?.inputSchema).not.toHaveProperty("properties.timeout.maximum") - // Code Mode receives the declared output schema, including the command output text. - expect(definition?.outputSchema).toHaveProperty("properties.output") - expect( - (yield* toolDefinitions(registry, [{ action: "shell", resource: "*", effect: "deny" }])).map( - (tool) => tool.name, - ), - ).not.toContain("shell") + productionIt.live( + "registers and returns real successful output from the active Location", + () => + Effect.acquireUseRelease( + Effect.promise(() => tmpdir()), + (tmp) => { + reset() + return withSession(tmp.path, (registry) => + Effect.gen(function* () { + const definitions = yield* toolDefinitions(registry) + const definition = definitions.find((tool) => tool.name === "shell") + expect(definition?.description).toStartWith("Execute a shell command and return its output.") + expect(definition?.inputSchema).not.toHaveProperty("properties.timeout.maximum") + // Code Mode receives the declared output schema, including the command output text. + expect(definition?.outputSchema).toHaveProperty("properties.output") + expect( + (yield* toolDefinitions(registry, [{ action: "shell", resource: "*", effect: "deny" }])).map( + (tool) => tool.name, + ), + ).not.toContain("shell") - const settled = yield* executeTool(registry, call({ command: helloCommand })) - expect(settled.status).toBe("completed") - expect(settled.metadata).toMatchObject({ exit: 0, truncated: false }) - expect(settled.content?.[0]).toEqual({ type: "text", text: "hello" }) - expect(settled.content?.[1]).toMatchObject({ - type: "text", - text: expect.stringContaining("Command exited with code 0."), - }) - expect(assertions).toMatchObject([ - { sessionID, action: "shell", resources: [isWindows ? "Start-Sleep -Milliseconds 100" : helloCommand] }, - ]) - expect(assertions[0]?.save).toEqual([isWindows ? "Start-Sleep *" : "printf *"]) - }), - ) - }, - (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)), - ), + const settled = yield* executeTool(registry, call({ command: helloCommand })) + expect(settled.status).toBe("completed") + expect(settled.metadata).toMatchObject({ exit: 0, truncated: false }) + expect(settled.content?.[0]).toEqual({ type: "text", text: "hello" }) + expect(settled.content?.[1]).toMatchObject({ + type: "text", + text: expect.stringContaining("Command exited with code 0."), + }) + expect(assertions).toMatchObject([ + { + sessionID, + action: "shell", + resources: [isWindows ? "Start-Sleep -Milliseconds 100" : helloCommand], + }, + ]) + expect(assertions[0]?.save).toEqual([isWindows ? "Start-Sleep *" : "printf *"]) + }), + ) + }, + (tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)), + ), + { timeout: 15_000 }, ) it.live("resolves a relative workdir from the active Location", () => @@ -577,7 +599,7 @@ describe("ShellTool", () => { ) it.live( - "does not repeat shell ID progress", + "reports shell ID progress once", () => Effect.acquireUseRelease( Effect.promise(() => tmpdir()), @@ -587,7 +609,7 @@ describe("ShellTool", () => { Effect.gen(function* () { const updates: Tool.Metadata[] = [] yield* executeTool(registry, { - ...call({ command: steadyProgressCommand }, "call-steady-progress"), + ...call({ command: helloCommand }, "call-shell-id-progress"), progress: (update) => Effect.sync(() => updates.push(update)), }) expect(updates).toHaveLength(1)