Files
openclaw/src/tasks/task-flow-registry.maintenance.test.ts
2026-04-28 23:09:37 +01:00

250 lines
7.3 KiB
TypeScript

import { afterEach, describe, expect, it } from "vitest";
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
import { createRunningTaskRun } from "./task-executor.js";
import {
createFlowRecord,
createManagedTaskFlow,
getTaskFlowById,
listTaskFlowRecords,
requestFlowCancel,
resetTaskFlowRegistryForTests,
} from "./task-flow-registry.js";
import {
getInspectableTaskFlowAuditSummary,
previewTaskFlowRegistryMaintenance,
runTaskFlowRegistryMaintenance,
} from "./task-flow-registry.maintenance.js";
import {
resetTaskRegistryDeliveryRuntimeForTests,
resetTaskRegistryForTests,
} from "./task-registry.js";
const ORIGINAL_STATE_DIR = process.env.OPENCLAW_STATE_DIR;
async function withTaskFlowMaintenanceStateDir(
run: (root: string) => Promise<void>,
): Promise<void> {
await withOpenClawTestState(
{
layout: "state-only",
prefix: "openclaw-task-flow-maintenance-",
},
async (state) => {
resetTaskRegistryDeliveryRuntimeForTests();
resetTaskRegistryForTests();
resetTaskFlowRegistryForTests();
try {
await run(state.stateDir);
} finally {
resetTaskRegistryDeliveryRuntimeForTests();
resetTaskRegistryForTests();
resetTaskFlowRegistryForTests();
}
},
);
}
describe("task-flow-registry maintenance", () => {
afterEach(() => {
if (ORIGINAL_STATE_DIR === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = ORIGINAL_STATE_DIR;
}
resetTaskRegistryDeliveryRuntimeForTests();
resetTaskRegistryForTests();
resetTaskFlowRegistryForTests();
});
it("finalizes cancel-requested managed flows once no child tasks remain active", async () => {
await withTaskFlowMaintenanceStateDir(async () => {
const flow = createManagedTaskFlow({
ownerKey: "agent:main:main",
controllerId: "tests/task-flow-maintenance",
goal: "Cancel work",
status: "running",
cancelRequestedAt: 100,
createdAt: 1,
updatedAt: 100,
});
expect(previewTaskFlowRegistryMaintenance()).toEqual({
reconciled: 1,
pruned: 0,
});
expect(await runTaskFlowRegistryMaintenance()).toEqual({
reconciled: 1,
pruned: 0,
});
expect(getTaskFlowById(flow.flowId)).toMatchObject({
flowId: flow.flowId,
status: "cancelled",
cancelRequestedAt: 100,
});
});
});
it("prunes old terminal flows", async () => {
await withTaskFlowMaintenanceStateDir(async () => {
const now = Date.now();
const oldFlow = createManagedTaskFlow({
ownerKey: "agent:main:main",
controllerId: "tests/task-flow-maintenance",
goal: "Old terminal flow",
status: "succeeded",
createdAt: now - 8 * 24 * 60 * 60_000,
updatedAt: now - 8 * 24 * 60 * 60_000,
endedAt: now - 8 * 24 * 60 * 60_000,
});
expect(previewTaskFlowRegistryMaintenance()).toEqual({
reconciled: 0,
pruned: 1,
});
expect(await runTaskFlowRegistryMaintenance()).toEqual({
reconciled: 0,
pruned: 1,
});
expect(getTaskFlowById(oldFlow.flowId)).toBeUndefined();
});
});
it("repairs terminal mirrored flows whose delivery updates outlived endedAt", async () => {
await withTaskFlowMaintenanceStateDir(async () => {
const flow = createFlowRecord({
syncMode: "task_mirrored",
ownerKey: "agent:main:main",
goal: "Failed ACP task",
status: "failed",
createdAt: 100,
updatedAt: 250,
endedAt: 200,
});
expect(getInspectableTaskFlowAuditSummary().byCode.inconsistent_timestamps).toBe(1);
expect(previewTaskFlowRegistryMaintenance()).toEqual({
reconciled: 1,
pruned: 0,
});
expect(await runTaskFlowRegistryMaintenance()).toEqual({
reconciled: 1,
pruned: 0,
});
expect(getTaskFlowById(flow.flowId)).toMatchObject({
endedAt: 200,
updatedAt: 200,
});
expect(getInspectableTaskFlowAuditSummary().byCode.inconsistent_timestamps).toBe(0);
});
});
it("does not finalize cancel-requested flows while a child task is still active", async () => {
await withTaskFlowMaintenanceStateDir(async () => {
const flow = createManagedTaskFlow({
ownerKey: "agent:main:main",
controllerId: "tests/task-flow-maintenance",
goal: "Wait for child cancel",
status: "running",
createdAt: 1,
updatedAt: 100,
});
const child = createRunningTaskRun({
runtime: "acp",
ownerKey: "agent:main:main",
scopeKind: "session",
parentFlowId: flow.flowId,
childSessionKey: "agent:main:child",
runId: "run-active-child",
task: "Inspect repo",
startedAt: 100,
lastEventAt: 100,
});
expect(
requestFlowCancel({
flowId: flow.flowId,
expectedRevision: flow.revision,
cancelRequestedAt: 100,
updatedAt: 100,
}),
).toMatchObject({
applied: true,
flow: expect.objectContaining({
flowId: flow.flowId,
cancelRequestedAt: 100,
}),
});
expect(previewTaskFlowRegistryMaintenance()).toEqual({
reconciled: 0,
pruned: 0,
});
expect(await runTaskFlowRegistryMaintenance()).toEqual({
reconciled: 0,
pruned: 0,
});
expect(getTaskFlowById(flow.flowId)).toMatchObject({
flowId: flow.flowId,
status: "running",
cancelRequestedAt: 100,
});
expect(child.parentFlowId).toBe(flow.flowId);
});
});
it("prunes many old terminal flows while keeping fresh and active ones", async () => {
await withTaskFlowMaintenanceStateDir(async () => {
const now = Date.now();
for (let index = 0; index < 25; index += 1) {
createManagedTaskFlow({
ownerKey: `agent:main:${index}`,
controllerId: "tests/task-flow-maintenance",
goal: `Old terminal flow ${index}`,
status: "succeeded",
createdAt: now - 8 * 24 * 60 * 60_000 - index,
updatedAt: now - 8 * 24 * 60 * 60_000 - index,
endedAt: now - 8 * 24 * 60 * 60_000 - index,
});
}
const fresh = createManagedTaskFlow({
ownerKey: "agent:main:fresh",
controllerId: "tests/task-flow-maintenance",
goal: "Fresh terminal flow",
status: "succeeded",
createdAt: now - 2 * 24 * 60 * 60_000,
updatedAt: now - 2 * 24 * 60 * 60_000,
endedAt: now - 2 * 24 * 60 * 60_000,
});
const running = createManagedTaskFlow({
ownerKey: "agent:main:running",
controllerId: "tests/task-flow-maintenance",
goal: "Active flow",
status: "running",
createdAt: now - 60_000,
updatedAt: now - 60_000,
});
expect(previewTaskFlowRegistryMaintenance()).toEqual({
reconciled: 0,
pruned: 25,
});
expect(await runTaskFlowRegistryMaintenance()).toEqual({
reconciled: 0,
pruned: 25,
});
const remainingFlowIds = new Set(listTaskFlowRecords().map((flow) => flow.flowId));
expect(remainingFlowIds).toEqual(new Set([fresh.flowId, running.flowId]));
});
});
});