test(effect): cover task resume and permissions
parent
9a5cf96b7a
commit
f4faab9bb2
|
|
@ -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<typeof SessionPrompt.prompt>[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<typeof SessionPrompt.prompt>[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<typeof SessionPrompt.prompt>[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"],
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue