mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-23 05:11:52 +00:00
268 lines
8.9 KiB
TypeScript
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();
|
|
});
|
|
});
|