From a142d76eecd5f137cb7f2dff4a9501ad8f6866ff Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 7 Jul 2026 11:53:22 -0400 Subject: [PATCH] test(core): reproduce explorer tool leak --- packages/core/test/location-layer.test.ts | 38 ++++++++++++++++++++++- packages/core/test/session-runner.test.ts | 31 ++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/packages/core/test/location-layer.test.ts b/packages/core/test/location-layer.test.ts index c538d1151f..42585c8e72 100644 --- a/packages/core/test/location-layer.test.ts +++ b/packages/core/test/location-layer.test.ts @@ -3,7 +3,7 @@ import path from "path" import { describe, expect } from "bun:test" import { Config } from "@opencode-ai/schema/config" import { Plugin } from "@opencode-ai/schema/plugin" -import { Context, DateTime, Effect, Equal, Hash, RcMap, Schema, Stream } from "effect" +import { Context, DateTime, Deferred, Effect, Equal, Hash, RcMap, Schema, Stream } from "effect" import { Plugin as EffectPlugin } from "@opencode-ai/plugin/v2/effect" import { AgentV2 } from "@opencode-ai/core/agent" import { Catalog } from "@opencode-ai/core/catalog" @@ -66,6 +66,42 @@ describe("LocationServiceMap", () => { ), ) + itWithSdk.live("does not advertise shell before explorer activation completes", () => + Effect.acquireRelease( + Effect.promise(() => tmpdir()), + (dir) => Effect.promise(() => dir[Symbol.asyncDispose]()), + ).pipe( + Effect.flatMap((dir) => + Effect.gen(function* () { + const started = yield* Deferred.make() + const release = yield* Deferred.make() + const sdk = yield* SdkPlugins.Service + yield* sdk.register( + EffectPlugin.define({ + id: "blocked-initial-activation", + effect: () => Deferred.succeed(started, undefined).pipe(Effect.andThen(Deferred.await(release))), + }), + ) + + const locations = yield* LocationServiceMap.Service + const context = yield* locations.contextEffect( + Location.Ref.make({ directory: AbsolutePath.make(dir.path) }), + ) + yield* Deferred.await(started) + + const tools = yield* Effect.gen(function* () { + const agents = yield* AgentV2.Service + const registry = yield* ToolRegistry.Service + const explorer = yield* agents.select("explore") + return yield* toolDefinitions(registry, explorer.info?.permissions) + }).pipe(Effect.provide(context), Effect.ensuring(Deferred.succeed(release, undefined))) + + expect(tools.map((tool) => tool.name)).not.toContain("shell") + }), + ), + ), + ) + it.live("applies ordered plugin config operations during boot", () => Effect.acquireRelease( Effect.promise(() => tmpdir()), diff --git a/packages/core/test/session-runner.test.ts b/packages/core/test/session-runner.test.ts index da8d9539c7..2b266bc509 100644 --- a/packages/core/test/session-runner.test.ts +++ b/packages/core/test/session-runner.test.ts @@ -1045,6 +1045,37 @@ describe("SessionRunnerLLM", () => { }), ) + it.effect("does not advertise unrestricted tools while the selected agent is unavailable", () => + Effect.gen(function* () { + yield* setup + const { db } = yield* Database.Service + const registry = yield* ToolRegistry.Service + yield* registry.register({ + shell: Tool.make({ + description: "Execute a shell command", + input: Schema.Struct({ command: Schema.String }), + output: Schema.Struct({}), + execute: () => Effect.succeed({}), + }), + }) + yield* db + .update(SessionTable) + .set({ agent: "explore" }) + .where(eq(SessionTable.id, sessionID)) + .run() + .pipe(Effect.orDie) + const session = yield* SessionV2.Service + yield* session.prompt({ sessionID, prompt: PromptInput.Prompt.make({ text: "Inspect files" }), resume: false }) + + requests.length = 0 + response = [] + yield* session.resume(sessionID) + + expect(requests).toHaveLength(1) + expect(requests[0]?.tools.map((tool) => tool.name)).not.toContain("shell") + }), + ) + it.effect("updates selected-agent skill guidance after an agent switch", () => Effect.gen(function* () { const session = yield* setup