Files
openclaw/src/gateway/server-methods/tasks.test.ts
2026-05-11 09:39:59 +01:00

216 lines
6.9 KiB
TypeScript

import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
createTaskRecord,
markTaskTerminalById,
recordTaskProgressByRunId,
resetTaskRegistryForTests,
} from "../../tasks/runtime-internal.js";
import { tasksHandlers } from "./tasks.js";
import type { RespondFn } from "./types.js";
const ORIGINAL_STATE_DIR = process.env.OPENCLAW_STATE_DIR;
type TaskResponsePayload = {
tasks?: Array<Record<string, unknown>>;
task?: Record<string, unknown>;
found?: boolean;
cancelled?: boolean;
};
let stateDir: string;
beforeEach(async () => {
stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-gateway-tasks-"));
process.env.OPENCLAW_STATE_DIR = stateDir;
resetTaskRegistryForTests();
});
afterEach(async () => {
resetTaskRegistryForTests();
if (ORIGINAL_STATE_DIR === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = ORIGINAL_STATE_DIR;
}
await fs.rm(stateDir, { recursive: true, force: true });
});
function captureRespond() {
const calls: Parameters<RespondFn>[] = [];
const respond: RespondFn = (...args) => {
calls.push(args);
};
return { calls, respond };
}
function createContext() {
return {
getRuntimeConfig: () => ({}),
} as never;
}
describe("tasks gateway handlers", () => {
it("lists task summaries with SDK-facing statuses and filters", async () => {
const running = createTaskRecord({
runtime: "subagent",
taskKind: "investigation",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
childSessionKey: "agent:worker:subagent:child",
agentId: "main",
runId: "run-running",
task: "Investigate issue",
status: "running",
deliveryStatus: "pending",
});
createTaskRecord({
runtime: "cli",
requesterSessionKey: "agent:other:main",
ownerKey: "agent:other:main",
scopeKind: "session",
runId: "run-other",
task: "Other task",
status: "running",
deliveryStatus: "pending",
});
const { calls, respond } = captureRespond();
await tasksHandlers["tasks.list"]({
req: { type: "req", id: "req-1", method: "tasks.list" },
params: {
status: "running",
agentId: "main",
sessionKey: "agent:main:main",
},
respond,
context: createContext(),
client: null,
isWebchatConnect: () => false,
});
expect(calls[0]?.[0]).toBe(true);
const payload = calls[0]?.[1] as TaskResponsePayload | undefined;
expect(payload?.tasks).toHaveLength(1);
const listedTask = payload?.tasks?.[0];
expect(listedTask?.id).toBe(running.taskId);
expect(listedTask?.taskId).toBe(running.taskId);
expect(listedTask?.kind).toBe("investigation");
expect(listedTask?.runtime).toBe("subagent");
expect(listedTask?.status).toBe("running");
expect(listedTask?.title).toBe("Investigate issue");
expect(listedTask?.agentId).toBe("main");
expect(listedTask?.sessionKey).toBe("agent:main:main");
expect(listedTask?.childSessionKey).toBe("agent:worker:subagent:child");
expect(listedTask?.runId).toBe("run-running");
});
it("gets completed tasks with stable completed status", async () => {
const task = createTaskRecord({
runtime: "cli",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
runId: "run-completed",
task: "Done task",
status: "succeeded",
deliveryStatus: "not_applicable",
});
const { calls, respond } = captureRespond();
await tasksHandlers["tasks.get"]({
req: { type: "req", id: "req-2", method: "tasks.get" },
params: { taskId: task.taskId },
respond,
context: createContext(),
client: null,
isWebchatConnect: () => false,
});
expect(calls[0]?.[0]).toBe(true);
const payload = calls[0]?.[1] as TaskResponsePayload | undefined;
expect(payload?.task?.id).toBe(task.taskId);
expect(payload?.task?.status).toBe("completed");
expect(payload?.task?.title).toBe("Done task");
});
it("sanitizes task text before exposing SDK summaries", async () => {
const task = createTaskRecord({
runtime: "cli",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
runId: "run-sanitized",
label:
"Compile artifact\nOpenClaw runtime context (internal): Keep internal details private.",
task: "Compile artifact",
status: "running",
deliveryStatus: "pending",
});
recordTaskProgressByRunId({
runId: "run-sanitized",
progressSummary:
"Bundling output\nOpenClaw runtime context (internal): Keep internal details private.",
});
markTaskTerminalById({
taskId: task.taskId,
status: "failed",
endedAt: Date.now(),
terminalSummary:
"Failed after build\nOpenClaw runtime context (internal): Keep internal details private.",
error: "Tool failed\nOpenClaw runtime context (internal): Keep internal details private.",
});
const { calls, respond } = captureRespond();
await tasksHandlers["tasks.get"]({
req: { type: "req", id: "req-sanitized", method: "tasks.get" },
params: { taskId: task.taskId },
respond,
context: createContext(),
client: null,
isWebchatConnect: () => false,
});
expect(calls[0]?.[0]).toBe(true);
const payload = calls[0]?.[1] as TaskResponsePayload | undefined;
expect(payload?.task?.id).toBe(task.taskId);
expect(payload?.task?.title).toBe("Compile artifact");
expect(payload?.task?.terminalSummary).toBe("Failed after build");
expect(payload?.task?.error).toBe("Tool failed");
expect(JSON.stringify(calls[0]?.[1])).not.toContain("OpenClaw runtime context");
});
it("cancels running task records and returns the updated task", async () => {
const task = createTaskRecord({
runtime: "cli",
requesterSessionKey: "agent:main:main",
ownerKey: "agent:main:main",
scopeKind: "session",
runId: "run-cancel",
task: "Cancelable task",
status: "running",
deliveryStatus: "pending",
});
const { calls, respond } = captureRespond();
await tasksHandlers["tasks.cancel"]({
req: { type: "req", id: "req-3", method: "tasks.cancel" },
params: { taskId: task.taskId, reason: "user stopped task" },
respond,
context: createContext(),
client: null,
isWebchatConnect: () => false,
});
expect(calls[0]?.[0]).toBe(true);
const payload = calls[0]?.[1] as TaskResponsePayload | undefined;
expect(payload?.found).toBe(true);
expect(payload?.cancelled).toBe(true);
expect(payload?.task?.id).toBe(task.taskId);
expect(payload?.task?.status).toBe("cancelled");
expect(payload?.task?.error).toBe("user stopped task");
});
});