openclaw/src/tasks/flow-registry.test.ts

249 lines
7.0 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { withTempDir } from "../test-helpers/temp-dir.js";
import {
createFlowRecord,
deleteFlowRecordById,
getFlowById,
listFlowRecords,
resetFlowRegistryForTests,
syncFlowFromTask,
updateFlowRecordById,
} from "./flow-registry.js";
const ORIGINAL_STATE_DIR = process.env.OPENCLAW_STATE_DIR;
async function withFlowRegistryTempDir<T>(run: (root: string) => Promise<T>): Promise<T> {
return await withTempDir({ prefix: "openclaw-flow-registry-" }, async (root) => {
process.env.OPENCLAW_STATE_DIR = root;
resetFlowRegistryForTests();
try {
return await run(root);
} finally {
// Close the sqlite-backed registry before Windows temp-dir cleanup removes the store root.
resetFlowRegistryForTests();
}
});
}
describe("flow-registry", () => {
beforeEach(() => {
vi.useRealTimers();
});
afterEach(() => {
vi.useRealTimers();
if (ORIGINAL_STATE_DIR === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = ORIGINAL_STATE_DIR;
}
resetFlowRegistryForTests();
});
it("creates, updates, lists, and deletes flow records", async () => {
await withFlowRegistryTempDir(async (root) => {
process.env.OPENCLAW_STATE_DIR = root;
resetFlowRegistryForTests();
const created = createFlowRecord({
ownerSessionKey: "agent:main:main",
goal: "Investigate flaky test",
status: "running",
currentStep: "spawn_task",
});
expect(getFlowById(created.flowId)).toMatchObject({
flowId: created.flowId,
status: "running",
currentStep: "spawn_task",
});
const updated = updateFlowRecordById(created.flowId, {
status: "waiting",
currentStep: "ask_user",
});
expect(updated).toMatchObject({
flowId: created.flowId,
status: "waiting",
currentStep: "ask_user",
});
expect(listFlowRecords()).toEqual([
expect.objectContaining({
flowId: created.flowId,
goal: "Investigate flaky test",
status: "waiting",
}),
]);
expect(deleteFlowRecordById(created.flowId)).toBe(true);
expect(getFlowById(created.flowId)).toBeUndefined();
expect(listFlowRecords()).toEqual([]);
});
});
it("lists newest flows first", async () => {
await withFlowRegistryTempDir(async (root) => {
process.env.OPENCLAW_STATE_DIR = root;
resetFlowRegistryForTests();
const earlier = createFlowRecord({
ownerSessionKey: "agent:main:main",
goal: "First flow",
createdAt: 100,
updatedAt: 100,
});
const later = createFlowRecord({
ownerSessionKey: "agent:main:main",
goal: "Second flow",
createdAt: 200,
updatedAt: 200,
});
expect(listFlowRecords().map((flow) => flow.flowId)).toEqual([later.flowId, earlier.flowId]);
});
});
it("applies minimal defaults for new flow records", async () => {
await withFlowRegistryTempDir(async (root) => {
process.env.OPENCLAW_STATE_DIR = root;
resetFlowRegistryForTests();
const created = createFlowRecord({
ownerSessionKey: "agent:main:main",
goal: "Background job",
});
expect(created).toMatchObject({
flowId: expect.any(String),
shape: "linear",
ownerSessionKey: "agent:main:main",
goal: "Background job",
status: "queued",
notifyPolicy: "done_only",
});
expect(created.currentStep).toBeUndefined();
expect(created.endedAt).toBeUndefined();
});
});
it("preserves endedAt when later updates change other flow fields", async () => {
await withFlowRegistryTempDir(async (root) => {
process.env.OPENCLAW_STATE_DIR = root;
resetFlowRegistryForTests();
const created = createFlowRecord({
ownerSessionKey: "agent:main:main",
goal: "Finish a task",
status: "succeeded",
endedAt: 456,
});
const updated = updateFlowRecordById(created.flowId, {
currentStep: "finish",
});
expect(updated).toMatchObject({
flowId: created.flowId,
currentStep: "finish",
endedAt: 456,
});
expect(getFlowById(created.flowId)).toMatchObject({
flowId: created.flowId,
endedAt: 456,
});
});
});
it("stores blocked metadata and clears it when a later task resumes the same flow", async () => {
await withFlowRegistryTempDir(async (root) => {
process.env.OPENCLAW_STATE_DIR = root;
resetFlowRegistryForTests();
const created = createFlowRecord({
shape: "single_task",
ownerSessionKey: "agent:main:main",
goal: "Fix permissions",
status: "running",
});
const blocked = syncFlowFromTask({
taskId: "task-blocked",
parentFlowId: created.flowId,
status: "succeeded",
terminalOutcome: "blocked",
notifyPolicy: "done_only",
label: "Fix permissions",
task: "Fix permissions",
lastEventAt: 200,
endedAt: 200,
terminalSummary: "Writable session required.",
});
expect(blocked).toMatchObject({
flowId: created.flowId,
status: "blocked",
blockedTaskId: "task-blocked",
blockedSummary: "Writable session required.",
endedAt: 200,
});
const resumed = syncFlowFromTask({
taskId: "task-retry",
parentFlowId: created.flowId,
status: "running",
notifyPolicy: "done_only",
label: "Fix permissions",
task: "Fix permissions",
lastEventAt: 260,
progressSummary: "Retrying with writable session",
});
expect(resumed).toMatchObject({
flowId: created.flowId,
status: "running",
});
expect(resumed?.blockedTaskId).toBeUndefined();
expect(resumed?.blockedSummary).toBeUndefined();
expect(resumed?.endedAt).toBeUndefined();
});
});
it("does not auto-sync linear flow state from linked child tasks", async () => {
await withFlowRegistryTempDir(async (root) => {
process.env.OPENCLAW_STATE_DIR = root;
resetFlowRegistryForTests();
const created = createFlowRecord({
ownerSessionKey: "agent:main:main",
goal: "Cluster PRs",
status: "waiting",
currentStep: "wait_for",
});
const synced = syncFlowFromTask({
taskId: "task-child",
parentFlowId: created.flowId,
status: "running",
notifyPolicy: "done_only",
label: "Child task",
task: "Child task",
lastEventAt: 250,
progressSummary: "Running child task",
});
expect(synced).toMatchObject({
flowId: created.flowId,
shape: "linear",
status: "waiting",
currentStep: "wait_for",
});
expect(getFlowById(created.flowId)).toMatchObject({
flowId: created.flowId,
status: "waiting",
currentStep: "wait_for",
});
});
});
});