56 lines
4.0 KiB
TypeScript
56 lines
4.0 KiB
TypeScript
import {afterEach, describe, expect, mock, test} from "bun:test";
|
|
const shared = await import("../agent-plugin-core/typescript/src/handoff.ts");
|
|
const run = mock(async (_script: URL, _bundle: unknown) => "Created Codex task");
|
|
mock.module("../agent-plugin-core/typescript/src/handoff.ts", () => ({...shared, runHandoff: run}));
|
|
const {createHandoffTool, activeMessages, registerHandoffCommand} = await import("./handoff");
|
|
afterEach(() => {run.mockReset(); run.mockResolvedValue("Created Codex task");});
|
|
const user = (id: string, text: string) => ({info: {role: "user", id}, parts: [{type: "text", text}]});
|
|
const toolMessage = (id: string, tool = "mem0_handoff") => ({info: {role: "assistant", id, time: {completed: 1}}, parts: [{type: "tool", callID: id, tool, state: {status: "running", input: {}}}]});
|
|
function tool(messages: any[]) {
|
|
return createHandoffTool({session: {
|
|
get: async () => ({data: {id: "native", title: "Native task", directory: "/tmp"}}),
|
|
messages: async () => ({data: messages}),
|
|
}} as any);
|
|
}
|
|
const context = {sessionID: "native", messageID: "handoff"} as any;
|
|
describe("native OpenCode handoff", () => {
|
|
test("exports current full context and skips only its own invocation", async () => {
|
|
const config: any = {};
|
|
registerHandoffCommand(config);
|
|
expect(config.command["mem0-handoff"].template).toContain("$ARGUMENTS");
|
|
expect(await tool([user("u", "x".repeat(20000)), toolMessage("handoff")]).execute({}, context)).toBe("Created Codex task");
|
|
const bundle = run.mock.calls[0][1] as any;
|
|
expect(bundle.source.session_id).toBe("native");
|
|
expect(bundle.items[0].content[0].text.length).toBe(20000);
|
|
expect(bundle.items).toHaveLength(1);
|
|
});
|
|
test("preserves completed tools and refuses any other unfinished tool", async () => {
|
|
const completed = {info: {role: "assistant", id: "done", time: {completed: 1}}, parts: [{type: "tool", callID: "done", tool: "read", state: {status: "completed", input: {path: "a"}, output: "full result", time: {}}}]};
|
|
await tool([user("u", "task"), completed, toolMessage("handoff")]).execute({}, context);
|
|
expect(JSON.stringify(run.mock.calls[0][1])).toContain("full result");
|
|
await expect(tool([user("u", "task"), toolMessage("other", "read"), toolMessage("handoff")]).execute({}, context)).rejects.toThrow("unfinished");
|
|
});
|
|
test("refuses a different unfinished assistant stream", async () => {
|
|
await expect(tool([user("u", "task"), {info: {role: "assistant", id: "stream", time: {}}, parts: [{type: "text", text: "partial"}]}]).execute({}, context)).rejects.toThrow("unfinished assistant");
|
|
});
|
|
test("rejects a failed assistant response even with a completed timestamp", async () => {
|
|
await expect(tool([user("u", "task"), {
|
|
info: {role: "assistant", id: "failed", time: {completed: 1}, error: {name: "MessageAbortedError", data: {message: "aborted"}}},
|
|
parts: [{type: "text", text: "partial answer"}],
|
|
}]).execute({}, context)).rejects.toThrow("interrupted or failed");
|
|
});
|
|
test("native compaction retains its summary and retained tail, excluding older history", () => {
|
|
const old = user("old", "old context");
|
|
const tail = user("tail", "retained");
|
|
const boundary = {info: {role: "user", id: "c"}, parts: [{type: "compaction", tail_start_id: "tail"}]};
|
|
const summary = {info: {role: "assistant", id: "s", parentID: "c", summary: true, finish: "stop"}, parts: [{type: "text", text: "Summary"}]};
|
|
expect(activeMessages([old, tail, boundary, summary, user("new", "continue")] as any).map(m => m.info.id)).toEqual(["c", "s", "tail", "new"]);
|
|
});
|
|
test("fails on unsupported remote media and propagates importer errors", async () => {
|
|
const image = {info: {role: "user", id: "u"}, parts: [{type: "file", mime: "image/png", url: "https://private/image.png"}]};
|
|
await expect(tool([image]).execute({}, context)).rejects.toThrow("unavailable locally");
|
|
run.mockRejectedValue(new Error("saved at /tmp/retry.json"));
|
|
await expect(tool([user("u", "hi")]).execute({}, context)).rejects.toThrow("saved at /tmp/retry.json");
|
|
});
|
|
});
|