build(plugins): pin retained runtime and cover handoff error paths

This commit is contained in:
kartik-mem0
2026-09-10 21:03:19 +05:30
parent 59939c003f
commit 1bf083dd04
9 changed files with 69 additions and 10 deletions
@@ -1,5 +1,5 @@
{
"revision": "d33b6604a4099c951207af9633d0bf0ddd20ed28",
"revision": "59939c003f6b3eb8add709e5897f7bfbe3e9f4d8",
"files": {
"handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11",
"handoff_sources.py": "dee34ca5a6cd591e2468b10108233adde3de0f7f2858e0b864f88ae6bed5e5f9"
@@ -1,5 +1,5 @@
{
"revision": "d33b6604a4099c951207af9633d0bf0ddd20ed28",
"revision": "59939c003f6b3eb8add709e5897f7bfbe3e9f4d8",
"files": {
"handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11",
"handoff_sources.py": "dee34ca5a6cd591e2468b10108233adde3de0f7f2858e0b864f88ae6bed5e5f9"
@@ -1,5 +1,5 @@
{
"revision": "d33b6604a4099c951207af9633d0bf0ddd20ed28",
"revision": "59939c003f6b3eb8add709e5897f7bfbe3e9f4d8",
"files": {
"handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11",
"handoff_sources.py": "dee34ca5a6cd591e2468b10108233adde3de0f7f2858e0b864f88ae6bed5e5f9"
@@ -1,5 +1,5 @@
{
"revision": "d33b6604a4099c951207af9633d0bf0ddd20ed28",
"revision": "59939c003f6b3eb8add709e5897f7bfbe3e9f4d8",
"files": {
"handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11",
"handoff_sources.py": "dee34ca5a6cd591e2468b10108233adde3de0f7f2858e0b864f88ae6bed5e5f9"
@@ -1,5 +1,5 @@
{
"revision": "d33b6604a4099c951207af9633d0bf0ddd20ed28",
"revision": "59939c003f6b3eb8add709e5897f7bfbe3e9f4d8",
"files": {
"handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11",
"handoff_sources.py": "dee34ca5a6cd591e2468b10108233adde3de0f7f2858e0b864f88ae6bed5e5f9"
@@ -1,5 +1,5 @@
{
"revision": "d33b6604a4099c951207af9633d0bf0ddd20ed28",
"revision": "59939c003f6b3eb8add709e5897f7bfbe3e9f4d8",
"files": {
"handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11",
"handoff_sources.py": "dee34ca5a6cd591e2468b10108233adde3de0f7f2858e0b864f88ae6bed5e5f9"
@@ -1,5 +1,5 @@
{
"revision": "d33b6604a4099c951207af9633d0bf0ddd20ed28",
"revision": "59939c003f6b3eb8add709e5897f7bfbe3e9f4d8",
"files": {
"handoff_engine.py": "5786e4f24e1145ce26867d18c78fafc8e3de4097b5494815073a2e09df15ea11",
"handoff_sources.py": "dee34ca5a6cd591e2468b10108233adde3de0f7f2858e0b864f88ae6bed5e5f9"
@@ -131,6 +131,10 @@ describe("loadCompactTriagePrompt", () => {
});
expect(prompt).toContain("No automatic recall happens in manual mode.");
const fullPrompt = loadTriagePrompt({recall: {strategy: "manual"}});
expect(fullPrompt).toContain("No automatic recall happens in manual mode.");
expect(fullPrompt).toContain(SEARCH_GUIDANCE);
expect(fullPrompt).not.toContain("ALWAYS rewrite");
});
it("includes short config summaries and truncates oversized custom rules", () => {
+58 -3
View File
@@ -6,14 +6,14 @@ import {runNativeSession, runHandoffAction} from "../../agent-plugin-core/typesc
import {registerHandoffCommand} from "../tools/handoff.ts";
vi.mock("../../agent-plugin-core/typescript/src/handoff.ts", async original => ({...await original<typeof import("../../agent-plugin-core/typescript/src/handoff.ts")>(), runNativeSession: vi.fn(), runHandoffAction: vi.fn()}));
let dir: string;
beforeEach(async () => {vi.clearAllMocks(); dir = await mkdtemp(join(tmpdir(), "openclaw-handoff-"));});
beforeEach(async () => {vi.resetAllMocks(); dir = await mkdtemp(join(tmpdir(), "openclaw-handoff-"));});
afterEach(async () => {await rm(dir, {recursive: true, force: true});});
function setup() {
function setup(context: {workspaceDir?: string} = {workspaceDir: "/tmp/native-project"}) {
const registerCommand = vi.fn();
const registerTool = vi.fn();
const hooks = new Map<string, any>();
registerHandoffCommand({registerCommand, registerTool, on: (name: string, handler: any) => hooks.set(name, handler)} as any);
return {cmd: registerCommand.mock.calls[0][0], tool: registerTool.mock.calls[0][0]({workspaceDir: "/tmp/native-project"}), hooks};
return {cmd: registerCommand.mock.calls[0][0], tool: registerTool.mock.calls[0][0](context), hooks};
}
it("saves the trusted current OpenClaw transcript from a user-only command", async () => {
const {cmd} = setup();
@@ -61,3 +61,58 @@ it("fails explicitly on unavailable context or resource errors", async () => {
expect((await cmd.handler({sessionFile: "/tmp/native.jsonl"})).text).toContain("invalid native transcript");
expect((await cmd.handler({args: "resume /tmp/shared.json", sessionFile: "/tmp/native.jsonl"})).text).toContain("identity is unavailable");
});
it("lists resources from the transcript project without queuing prompt context", async () => {
const {cmd, hooks} = setup();
const sessionFile = join(dir, "native.jsonl");
await writeFile(sessionFile, JSON.stringify({type: "session", cwd: dir}) + "\n");
vi.mocked(runHandoffAction).mockResolvedValue("Available shared resource");
expect(await cmd.handler({args: "list", sessionFile, sessionId: "native"})).toEqual({text: "Available shared resource"});
expect(runHandoffAction).toHaveBeenCalledWith(expect.any(URL), "list", dir, undefined);
expect(hooks.get("before_prompt_build")({}, {sessionId: "native"})).toBeUndefined();
});
it.each([
["empty transcript", ""],
["non-session header", JSON.stringify({type: "message", cwd: "/tmp"})],
["missing cwd", JSON.stringify({type: "session"})],
["relative cwd", JSON.stringify({type: "session", cwd: "relative/project"})],
])("rejects %s before accessing shared resources", async (_description, content) => {
const {cmd, hooks} = setup();
const sessionFile = join(dir, "invalid.jsonl");
await writeFile(sessionFile, content);
const result = await cmd.handler({args: "resume /tmp/shared.json", sessionFile, sessionId: "native"});
expect(result.text).toContain("project directory is unavailable");
expect(runHandoffAction).not.toHaveBeenCalled();
expect(hooks.get("before_prompt_build")({}, {sessionId: "native"})).toBeUndefined();
});
it("reports a missing transcript rather than guessing a project", async () => {
const {cmd} = setup();
expect((await cmd.handler({args: "list", sessionFile: join(dir, "missing.jsonl")})).text).toContain("ENOENT");
expect(runHandoffAction).not.toHaveBeenCalled();
});
it("rejects tools without a native workspace and unsupported actions", async () => {
const missing = setup({}).tool;
expect(await missing.execute("call", {action: "list"})).toMatchObject({isError: true, content: [{text: expect.stringContaining("project directory is unavailable")}]});
const {tool} = setup();
expect(await tool.execute("call", {action: "save"})).toMatchObject({isError: true, content: [{text: expect.stringContaining("Choose list or resume")}]});
expect(runHandoffAction).not.toHaveBeenCalled();
});
it("lists through the tool without requiring a resource path", async () => {
const {tool} = setup();
vi.mocked(runHandoffAction).mockResolvedValue("No shared resources");
expect(await tool.execute("call", {action: "list"})).toEqual({content: [{type: "text", text: "No shared resources"}]});
expect(runHandoffAction).toHaveBeenCalledWith(expect.any(URL), "list", "/tmp/native-project", undefined);
});
it.each([new Error("Resource cannot be read"), "Resource cannot be read"])("surfaces resource failures as tool errors: %s", async (error) => {
const {tool} = setup();
vi.mocked(runHandoffAction).mockRejectedValue(error);
expect(await tool.execute("call", {action: "resume", resource: "/tmp/shared.json"})).toEqual({isError: true, content: [{type: "text", text: "Session handoff failed: Resource cannot be read"}]});
});
it("reports a non-Error command failure without injecting context", async () => {
const {cmd, hooks} = setup();
const sessionFile = join(dir, "native.jsonl");
await writeFile(sessionFile, JSON.stringify({type: "session", cwd: dir}) + "\n");
vi.mocked(runHandoffAction).mockRejectedValue("Resource cannot be read");
expect(await cmd.handler({args: "resume /tmp/shared.json", sessionFile, sessionId: "native"})).toEqual({text: "Session handoff failed: Resource cannot be read"});
expect(hooks.get("before_prompt_build")({}, {sessionId: "native"})).toBeUndefined();
});