diff --git a/packages/opencode/test/tool/task.test.ts b/packages/opencode/test/tool/task.test.ts index d00f89da75..e2abdc138e 100644 --- a/packages/opencode/test/tool/task.test.ts +++ b/packages/opencode/test/tool/task.test.ts @@ -4,6 +4,11 @@ import { Agent } from "../../src/agent/agent" import { Config } from "../../src/config/config" import * as CrossSpawnSpawner from "../../src/effect/cross-spawn-spawner" import { Instance } from "../../src/project/instance" +import { Session } from "../../src/session" +import { MessageV2 } from "../../src/session/message-v2" +import { SessionPrompt } from "../../src/session/prompt" +import { MessageID, PartID } from "../../src/session/schema" +import { ModelID, ProviderID } from "../../src/provider/schema" import { TaskTool } from "../../src/tool/task" import { provideTmpdirInstance } from "../fixture/fixture" import { testEffect } from "../lib/effect" @@ -12,7 +17,73 @@ afterEach(async () => { await Instance.disposeAll() }) -const it = testEffect(Layer.mergeAll(Agent.defaultLayer, Config.defaultLayer, CrossSpawnSpawner.defaultLayer)) +const ref = { + providerID: ProviderID.make("test"), + modelID: ModelID.make("test-model"), +} + +const it = testEffect( + Layer.mergeAll(Agent.defaultLayer, Config.defaultLayer, CrossSpawnSpawner.defaultLayer, Session.defaultLayer), +) + +const seed = Effect.fn("TaskToolTest.seed")(function* (title = "Pinned") { + const session = yield* Session.Service + const chat = yield* session.create({ title }) + const user = yield* session.updateMessage({ + id: MessageID.ascending(), + role: "user", + sessionID: chat.id, + agent: "build", + model: ref, + time: { created: Date.now() }, + }) + const assistant: MessageV2.Assistant = { + id: MessageID.ascending(), + role: "assistant", + parentID: user.id, + sessionID: chat.id, + mode: "build", + agent: "build", + cost: 0, + path: { cwd: "/tmp", root: "/tmp" }, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: ref.modelID, + providerID: ref.providerID, + time: { created: Date.now() }, + } + yield* session.updateMessage(assistant) + return { chat, assistant } +}) + +function reply(input: Parameters[0], text: string): MessageV2.WithParts { + const id = MessageID.ascending() + return { + info: { + id, + role: "assistant", + parentID: input.messageID ?? MessageID.ascending(), + sessionID: input.sessionID, + mode: input.agent ?? "general", + agent: input.agent ?? "general", + cost: 0, + path: { cwd: "/tmp", root: "/tmp" }, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + modelID: input.model?.modelID ?? ref.modelID, + providerID: input.model?.providerID ?? ref.providerID, + time: { created: Date.now() }, + finish: "stop", + }, + parts: [ + { + id: PartID.ascending(), + messageID: id, + sessionID: input.sessionID, + type: "text", + text, + }, + ], + } +} describe("tool.task", () => { it.live("description sorts subagents by name and is stable across calls", () => @@ -53,4 +124,144 @@ describe("tool.task", () => { }, ), ) + + it.live("execute resumes an existing task session from task_id", () => + provideTmpdirInstance(() => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const child = yield* sessions.create({ parentID: chat.id, title: "Existing child" }) + const tool = yield* TaskTool + const def = yield* Effect.promise(() => tool.init()) + const resolve = SessionPrompt.resolvePromptParts + const prompt = SessionPrompt.prompt + let seen: Parameters[0] | undefined + + SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }] + SessionPrompt.prompt = async (input) => { + seen = input + return reply(input, "resumed") + } + yield* Effect.addFinalizer(() => + Effect.sync(() => { + SessionPrompt.resolvePromptParts = resolve + SessionPrompt.prompt = prompt + }), + ) + + const result = yield* Effect.promise(() => + def.execute( + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "general", + task_id: child.id, + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + messages: [], + metadata() {}, + ask: async () => {}, + }, + ), + ) + + const kids = yield* sessions.children(chat.id) + expect(kids).toHaveLength(1) + expect(kids[0]?.id).toBe(child.id) + expect(result.metadata.sessionId).toBe(child.id) + expect(result.output).toContain(`task_id: ${child.id}`) + expect(seen?.sessionID).toBe(child.id) + }), + ), + ) + + it.live("execute shapes child permissions for task, todowrite, and primary tools", () => + provideTmpdirInstance( + () => + Effect.gen(function* () { + const sessions = yield* Session.Service + const { chat, assistant } = yield* seed() + const tool = yield* TaskTool + const def = yield* Effect.promise(() => tool.init()) + const resolve = SessionPrompt.resolvePromptParts + const prompt = SessionPrompt.prompt + let seen: Parameters[0] | undefined + + SessionPrompt.resolvePromptParts = async (template) => [{ type: "text", text: template }] + SessionPrompt.prompt = async (input) => { + seen = input + return reply(input, "done") + } + yield* Effect.addFinalizer(() => + Effect.sync(() => { + SessionPrompt.resolvePromptParts = resolve + SessionPrompt.prompt = prompt + }), + ) + + const result = yield* Effect.promise(() => + def.execute( + { + description: "inspect bug", + prompt: "look into the cache key path", + subagent_type: "reviewer", + }, + { + sessionID: chat.id, + messageID: assistant.id, + agent: "build", + abort: new AbortController().signal, + messages: [], + metadata() {}, + ask: async () => {}, + }, + ), + ) + + const child = yield* sessions.get(result.metadata.sessionId) + expect(child.parentID).toBe(chat.id) + expect(child.permission).toEqual([ + { + permission: "todowrite", + pattern: "*", + action: "deny", + }, + { + permission: "bash", + pattern: "*", + action: "allow", + }, + { + permission: "read", + pattern: "*", + action: "allow", + }, + ]) + expect(seen?.tools).toEqual({ + todowrite: false, + bash: false, + read: false, + }) + }), + { + config: { + agent: { + reviewer: { + mode: "subagent", + permission: { + task: "allow", + }, + }, + }, + experimental: { + primary_tools: ["bash", "read"], + }, + }, + }, + ), + ) })