Files
mem0/integrations/opencode-plugin/handoff.test.ts
T

66 lines
4.8 KiB
TypeScript

import {afterEach, describe, expect, mock, test} from "bun:test";
const shared = await import("../agent-plugin-core/typescript/src/handoff.ts");
const actionRun = mock(async (_script: URL, _action: string, _cwd: string, _resource?: string) => "Full historical context");
const run = mock(async (_script: URL, _bundle: unknown) => "Saved shared resource");
mock.module("../agent-plugin-core/typescript/src/handoff.ts", () => ({...shared, runHandoff: run, runHandoffAction: actionRun}));
const {createHandoffTool, activeMessages, registerHandoffCommand} = await import("./handoff");
afterEach(() => {actionRun.mockClear(); run.mockReset(); run.mockResolvedValue("Saved shared resource");});
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", directory: "/tmp/native-project"} 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("Saved shared resource");
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");
});
});
test("list and resume return shared history to the current model using its native project", async () => {
for (const action of ["list", "resume"] as const) {
const native = createHandoffTool({session: {get: () => {throw new Error("should not export");}}} as any);
expect(await native.execute({action, resource: "/tmp/shared task.json"}, context)).toBe("Full historical context");
expect(actionRun).toHaveBeenLastCalledWith(expect.any(URL), action, "/tmp/native-project", "/tmp/shared task.json");
}
expect(run).not.toHaveBeenCalled();
});