mirror of
https://github.com/anomalyco/opencode.git
synced 2026-05-09 11:10:56 +00:00
278 lines
8.3 KiB
TypeScript
278 lines
8.3 KiB
TypeScript
import { afterEach, describe, expect } from "bun:test"
|
|
import { Effect, Layer, Scope } from "effect"
|
|
import { Agent } from "../../src/agent/agent"
|
|
import { CrossSpawnSpawner } from "@opencode-ai/core/cross-spawn-spawner"
|
|
import { Instance } from "../../src/project/instance"
|
|
import { Session } from "@/session/session"
|
|
import { MessageV2 } from "../../src/session/message-v2"
|
|
import { MessageID, PartID } from "../../src/session/schema"
|
|
import { SessionStatus } from "../../src/session/status"
|
|
import { TaskStatusTool } from "../../src/tool/task_status"
|
|
import { Truncate } from "@/tool/truncate"
|
|
import { ModelID, ProviderID } from "../../src/provider/schema"
|
|
import { provideTmpdirInstance } from "../fixture/fixture"
|
|
import { testEffect } from "../lib/effect"
|
|
|
|
afterEach(async () => {
|
|
await Instance.disposeAll()
|
|
})
|
|
|
|
const ref = {
|
|
providerID: ProviderID.make("test"),
|
|
modelID: ModelID.make("test-model"),
|
|
}
|
|
|
|
const it = testEffect(
|
|
Layer.mergeAll(
|
|
Agent.defaultLayer,
|
|
CrossSpawnSpawner.defaultLayer,
|
|
Session.defaultLayer,
|
|
SessionStatus.defaultLayer,
|
|
Truncate.defaultLayer,
|
|
),
|
|
)
|
|
|
|
const seedUser = Effect.fn("TaskStatusToolTest.seedUser")(function* (sessionID: Session.Info["id"]) {
|
|
const session = yield* Session.Service
|
|
return yield* session.updateMessage({
|
|
id: MessageID.ascending(),
|
|
role: "user",
|
|
sessionID,
|
|
agent: "build",
|
|
model: ref,
|
|
time: { created: Date.now() },
|
|
})
|
|
})
|
|
|
|
const seedAssistant = Effect.fn("TaskStatusToolTest.seedAssistant")(function* (input: {
|
|
sessionID: Session.Info["id"]
|
|
text: string
|
|
error?: string
|
|
}) {
|
|
const session = yield* Session.Service
|
|
const user = yield* seedUser(input.sessionID)
|
|
const message = yield* session.updateMessage({
|
|
id: MessageID.ascending(),
|
|
role: "assistant",
|
|
parentID: user.id,
|
|
sessionID: input.sessionID,
|
|
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(), completed: Date.now() },
|
|
finish: "stop",
|
|
...(input.error
|
|
? {
|
|
error: new MessageV2.APIError({
|
|
message: input.error,
|
|
isRetryable: false,
|
|
}).toObject(),
|
|
}
|
|
: {}),
|
|
})
|
|
|
|
yield* session.updatePart({
|
|
id: PartID.ascending(),
|
|
messageID: message.id,
|
|
sessionID: input.sessionID,
|
|
type: "text",
|
|
text: input.text,
|
|
})
|
|
})
|
|
|
|
describe("tool.task_status", () => {
|
|
it.live("returns running while session status is busy", () =>
|
|
provideTmpdirInstance(() =>
|
|
Effect.gen(function* () {
|
|
const sessions = yield* Session.Service
|
|
const status = yield* SessionStatus.Service
|
|
const tool = yield* TaskStatusTool
|
|
const def = yield* tool.init()
|
|
const chat = yield* sessions.create({})
|
|
|
|
yield* status.set(chat.id, { type: "busy" })
|
|
const result = yield* def.execute(
|
|
{ task_id: chat.id },
|
|
{
|
|
sessionID: chat.id,
|
|
messageID: MessageID.ascending(),
|
|
agent: "build",
|
|
abort: new AbortController().signal,
|
|
messages: [],
|
|
metadata: () => Effect.void,
|
|
ask: () => Effect.void,
|
|
},
|
|
)
|
|
|
|
expect(result.output).toContain("state: running")
|
|
}),
|
|
),
|
|
)
|
|
|
|
it.live("returns completed with final task output", () =>
|
|
provideTmpdirInstance(() =>
|
|
Effect.gen(function* () {
|
|
const sessions = yield* Session.Service
|
|
const tool = yield* TaskStatusTool
|
|
const def = yield* tool.init()
|
|
const chat = yield* sessions.create({})
|
|
|
|
yield* seedAssistant({ sessionID: chat.id, text: "all done" })
|
|
|
|
const result = yield* def.execute(
|
|
{ task_id: chat.id },
|
|
{
|
|
sessionID: chat.id,
|
|
messageID: MessageID.ascending(),
|
|
agent: "build",
|
|
abort: new AbortController().signal,
|
|
messages: [],
|
|
metadata: () => Effect.void,
|
|
ask: () => Effect.void,
|
|
},
|
|
)
|
|
|
|
expect(result.output).toContain("state: completed")
|
|
expect(result.output).toContain("all done")
|
|
}),
|
|
),
|
|
)
|
|
|
|
it.live("wait=true blocks until terminal status", () =>
|
|
provideTmpdirInstance(() =>
|
|
Effect.gen(function* () {
|
|
const sessions = yield* Session.Service
|
|
const status = yield* SessionStatus.Service
|
|
const tool = yield* TaskStatusTool
|
|
const def = yield* tool.init()
|
|
const chat = yield* sessions.create({})
|
|
const scope = yield* Scope.Scope
|
|
|
|
yield* status.set(chat.id, { type: "busy" })
|
|
yield* Effect.gen(function* () {
|
|
yield* Effect.sleep("150 millis")
|
|
yield* status.set(chat.id, { type: "idle" })
|
|
yield* seedAssistant({ sessionID: chat.id, text: "finished later" })
|
|
}).pipe(Effect.forkIn(scope))
|
|
|
|
const result = yield* def.execute(
|
|
{
|
|
task_id: chat.id,
|
|
wait: true,
|
|
timeout_ms: 4_000,
|
|
},
|
|
{
|
|
sessionID: chat.id,
|
|
messageID: MessageID.ascending(),
|
|
agent: "build",
|
|
abort: new AbortController().signal,
|
|
messages: [],
|
|
metadata: () => Effect.void,
|
|
ask: () => Effect.void,
|
|
},
|
|
)
|
|
|
|
expect(result.output).toContain("state: completed")
|
|
expect(result.output).toContain("finished later")
|
|
}),
|
|
),
|
|
)
|
|
|
|
it.live("returns error when child run fails", () =>
|
|
provideTmpdirInstance(() =>
|
|
Effect.gen(function* () {
|
|
const sessions = yield* Session.Service
|
|
const tool = yield* TaskStatusTool
|
|
const def = yield* tool.init()
|
|
const chat = yield* sessions.create({})
|
|
|
|
yield* seedAssistant({ sessionID: chat.id, text: "", error: "child failed" })
|
|
|
|
const result = yield* def.execute(
|
|
{ task_id: chat.id },
|
|
{
|
|
sessionID: chat.id,
|
|
messageID: MessageID.ascending(),
|
|
agent: "build",
|
|
abort: new AbortController().signal,
|
|
messages: [],
|
|
metadata: () => Effect.void,
|
|
ask: () => Effect.void,
|
|
},
|
|
)
|
|
|
|
expect(result.output).toContain("state: error")
|
|
expect(result.output).toContain("child failed")
|
|
expect(result.metadata.state).toBe("error")
|
|
}),
|
|
),
|
|
)
|
|
|
|
it.live("wait=true times out with timed_out metadata", () =>
|
|
provideTmpdirInstance(() =>
|
|
Effect.gen(function* () {
|
|
const sessions = yield* Session.Service
|
|
const status = yield* SessionStatus.Service
|
|
const tool = yield* TaskStatusTool
|
|
const def = yield* tool.init()
|
|
const chat = yield* sessions.create({})
|
|
|
|
yield* status.set(chat.id, { type: "busy" })
|
|
const result = yield* def.execute(
|
|
{
|
|
task_id: chat.id,
|
|
wait: true,
|
|
timeout_ms: 80,
|
|
},
|
|
{
|
|
sessionID: chat.id,
|
|
messageID: MessageID.ascending(),
|
|
agent: "build",
|
|
abort: new AbortController().signal,
|
|
messages: [],
|
|
metadata: () => Effect.void,
|
|
ask: () => Effect.void,
|
|
},
|
|
)
|
|
|
|
expect(result.output).toContain("Timed out after 80ms")
|
|
expect(result.metadata.timed_out).toBe(true)
|
|
expect(result.metadata.state).toBe("running")
|
|
}),
|
|
),
|
|
)
|
|
|
|
it.live("returns running for resumed task with a newer user turn", () =>
|
|
provideTmpdirInstance(() =>
|
|
Effect.gen(function* () {
|
|
const sessions = yield* Session.Service
|
|
const tool = yield* TaskStatusTool
|
|
const def = yield* tool.init()
|
|
const chat = yield* sessions.create({})
|
|
|
|
yield* seedAssistant({ sessionID: chat.id, text: "old done" })
|
|
yield* seedUser(chat.id)
|
|
|
|
const result = yield* def.execute(
|
|
{ task_id: chat.id },
|
|
{
|
|
sessionID: chat.id,
|
|
messageID: MessageID.ascending(),
|
|
agent: "build",
|
|
abort: new AbortController().signal,
|
|
messages: [],
|
|
metadata: () => Effect.void,
|
|
ask: () => Effect.void,
|
|
},
|
|
)
|
|
|
|
expect(result.output).toContain("state: running")
|
|
expect(result.output).toContain("Task is starting.")
|
|
}),
|
|
),
|
|
)
|
|
})
|