Files
openclaw/src/plugins/runtime/runtime-tasks.test.ts
2026-05-11 20:35:33 +01:00

268 lines
8.9 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import {
getDetachedTaskLifecycleRuntime,
setDetachedTaskLifecycleRuntime,
} from "../../tasks/detached-task-runtime.js";
import {
getRuntimeTaskMocks,
installRuntimeTaskDeliveryMock,
resetRuntimeTaskTestState,
} from "./runtime-task-test-harness.js";
import { createRuntimeTaskFlow } from "./runtime-taskflow.js";
import { createRuntimeTaskFlows, createRuntimeTaskRuns } from "./runtime-tasks.js";
const runtimeTaskMocks = getRuntimeTaskMocks();
afterEach(() => {
resetRuntimeTaskTestState();
});
function requireRecord(value: unknown): Record<string, unknown> {
if (!value || typeof value !== "object" || Array.isArray(value)) {
throw new Error("Expected a non-array record");
}
return value as Record<string, unknown>;
}
function requireRecordById(items: readonly unknown[], id: string): Record<string, unknown> {
for (const item of items) {
const record = requireRecord(item);
if (record.id === id) {
return record;
}
}
throw new Error(`Missing record ${id}`);
}
describe("runtime tasks", () => {
beforeEach(() => {
installRuntimeTaskDeliveryMock();
});
it("exposes canonical task and TaskFlow DTOs without leaking raw registry fields", () => {
const legacyTaskFlow = createRuntimeTaskFlow().bindSession({
sessionKey: "agent:main:main",
requesterOrigin: {
channel: "telegram",
to: "telegram:123",
},
});
const taskFlows = createRuntimeTaskFlows().bindSession({
sessionKey: "agent:main:main",
});
const taskRuns = createRuntimeTaskRuns().bindSession({
sessionKey: "agent:main:main",
});
const otherTaskFlows = createRuntimeTaskFlows().bindSession({
sessionKey: "agent:main:other",
});
const otherTaskRuns = createRuntimeTaskRuns().bindSession({
sessionKey: "agent:main:other",
});
const created = legacyTaskFlow.createManaged({
controllerId: "tests/runtime-tasks",
goal: "Review inbox",
currentStep: "triage",
stateJson: { lane: "priority" },
});
const child = legacyTaskFlow.runTask({
flowId: created.flowId,
runtime: "acp",
childSessionKey: "agent:main:subagent:child",
runId: "runtime-task-run",
label: "Inbox triage",
task: "Review PR 1",
status: "running",
startedAt: 10,
lastEventAt: 11,
progressSummary: "Inspecting",
});
if (!child.created) {
throw new Error("expected child task creation to succeed");
}
const listedFlow = requireRecordById(taskFlows.list(), created.flowId);
expect(listedFlow.ownerKey).toBe("agent:main:main");
expect(listedFlow.goal).toBe("Review inbox");
expect(listedFlow.currentStep).toBe("triage");
const flow = requireRecord(taskFlows.get(created.flowId));
expect(flow.id).toBe(created.flowId);
expect(flow.ownerKey).toBe("agent:main:main");
expect(flow.goal).toBe("Review inbox");
expect(flow.currentStep).toBe("triage");
expect(flow.state).toEqual({ lane: "priority" });
const taskSummary = requireRecord(flow.taskSummary);
expect(taskSummary.total).toBe(1);
expect(taskSummary.active).toBe(1);
const flowTasks = flow.tasks;
expect(Array.isArray(flowTasks)).toBe(true);
const flowTask = requireRecordById(flowTasks as unknown[], child.task.taskId);
expect(flowTask.flowId).toBe(created.flowId);
expect(flowTask.title).toBe("Review PR 1");
expect(flowTask.label).toBe("Inbox triage");
expect(flowTask.runId).toBe("runtime-task-run");
const listedRun = requireRecordById(taskRuns.list(), child.task.taskId);
expect(listedRun.flowId).toBe(created.flowId);
expect(listedRun.sessionKey).toBe("agent:main:main");
expect(listedRun.title).toBe("Review PR 1");
expect(listedRun.status).toBe("running");
const taskRun = requireRecord(taskRuns.get(child.task.taskId));
expect(taskRun.id).toBe(child.task.taskId);
expect(taskRun.flowId).toBe(created.flowId);
expect(taskRun.title).toBe("Review PR 1");
expect(taskRun.progressSummary).toBe("Inspecting");
expect(taskRuns.findLatest()?.id).toBe(child.task.taskId);
expect(taskRuns.resolve("runtime-task-run")?.id).toBe(child.task.taskId);
const summary = requireRecord(taskFlows.getTaskSummary(created.flowId));
expect(summary.total).toBe(1);
expect(summary.active).toBe(1);
expect(otherTaskFlows.get(created.flowId)).toBeUndefined();
expect(otherTaskRuns.get(child.task.taskId)).toBeUndefined();
const flowDetail = taskFlows.get(created.flowId);
expect(flowDetail).not.toHaveProperty("revision");
expect(flowDetail).not.toHaveProperty("controllerId");
expect(flowDetail).not.toHaveProperty("syncMode");
const taskDetail = taskRuns.get(child.task.taskId);
expect(taskDetail).not.toHaveProperty("taskId");
expect(taskDetail).not.toHaveProperty("requesterSessionKey");
expect(taskDetail).not.toHaveProperty("scopeKind");
});
it("maps task cancellation results onto canonical task DTOs", async () => {
const legacyTaskFlow = createRuntimeTaskFlow().bindSession({
sessionKey: "agent:main:main",
});
const taskRuns = createRuntimeTaskRuns().bindSession({
sessionKey: "agent:main:main",
});
const created = legacyTaskFlow.createManaged({
controllerId: "tests/runtime-tasks",
goal: "Cancel active task",
});
const child = legacyTaskFlow.runTask({
flowId: created.flowId,
runtime: "acp",
childSessionKey: "agent:main:subagent:child",
runId: "runtime-task-cancel",
task: "Cancel me",
status: "running",
startedAt: 20,
lastEventAt: 21,
});
if (!child.created) {
throw new Error("expected child task creation to succeed");
}
const result = await taskRuns.cancel({
taskId: child.task.taskId,
cfg: {} as never,
});
expect(runtimeTaskMocks.cancelSessionMock).toHaveBeenCalledWith({
cfg: {},
sessionKey: "agent:main:subagent:child",
reason: "task-cancel",
});
expect(result.found).toBe(true);
expect(result.cancelled).toBe(true);
const task = requireRecord(result.task);
expect(task.id).toBe(child.task.taskId);
expect(task.title).toBe("Cancel me");
expect(task.status).toBe("cancelled");
});
it("routes runtime task cancellation through the detached task runtime seam", async () => {
const legacyTaskFlow = createRuntimeTaskFlow().bindSession({
sessionKey: "agent:main:main",
});
const taskRuns = createRuntimeTaskRuns().bindSession({
sessionKey: "agent:main:main",
});
const created = legacyTaskFlow.createManaged({
controllerId: "tests/runtime-tasks",
goal: "Cancel through runtime seam",
});
const child = legacyTaskFlow.runTask({
flowId: created.flowId,
runtime: "acp",
childSessionKey: "agent:main:subagent:child",
runId: "runtime-task-cancel-seam",
task: "Cancel via seam",
status: "running",
startedAt: 22,
lastEventAt: 23,
});
if (!child.created) {
throw new Error("expected child task creation to succeed");
}
const defaultRuntime = getDetachedTaskLifecycleRuntime();
const cancelDetachedTaskRunByIdSpy = vi.fn(
(...args: Parameters<typeof defaultRuntime.cancelDetachedTaskRunById>) =>
defaultRuntime.cancelDetachedTaskRunById(...args),
);
setDetachedTaskLifecycleRuntime({
...defaultRuntime,
cancelDetachedTaskRunById: cancelDetachedTaskRunByIdSpy,
});
await taskRuns.cancel({
taskId: child.task.taskId,
cfg: {} as never,
});
expect(cancelDetachedTaskRunByIdSpy).toHaveBeenCalledWith({
cfg: {} as never,
taskId: child.task.taskId,
});
});
it("does not allow cross-owner task cancellation or leak task details", async () => {
const legacyTaskFlow = createRuntimeTaskFlow().bindSession({
sessionKey: "agent:main:main",
});
const otherTaskRuns = createRuntimeTaskRuns().bindSession({
sessionKey: "agent:main:other",
});
const created = legacyTaskFlow.createManaged({
controllerId: "tests/runtime-tasks",
goal: "Keep owner isolation",
});
const child = legacyTaskFlow.runTask({
flowId: created.flowId,
runtime: "acp",
childSessionKey: "agent:main:subagent:child",
runId: "runtime-task-isolation",
task: "Do not cancel me",
status: "running",
startedAt: 30,
lastEventAt: 31,
});
if (!child.created) {
throw new Error("expected child task creation to succeed");
}
const result = await otherTaskRuns.cancel({
taskId: child.task.taskId,
cfg: {} as never,
});
expect(runtimeTaskMocks.cancelSessionMock).not.toHaveBeenCalled();
expect(result).toEqual({
found: false,
cancelled: false,
reason: "Task not found.",
});
expect(otherTaskRuns.get(child.task.taskId)).toBeUndefined();
});
});