mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-16 15:43:57 -06:00
3d76a4af92
* refactor(sessions): privatize runtime test seams * refactor(commitments): privatize extraction test seams * refactor(flows): privatize doctor health seams * refactor(tasks): privatize registry test seams * build(commitments): bundle Docker test support * chore(deadcode): shrink runtime export baseline
282 lines
8.1 KiB
TypeScript
282 lines
8.1 KiB
TypeScript
// Tasks JSON tests cover structured task command output and managed task flow state.
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { RuntimeEnv } from "../runtime.js";
|
|
import { createManagedTaskFlow as createManagedTaskFlowOrNull } from "../tasks/task-flow-registry.js";
|
|
import type { TaskFlowRecord } from "../tasks/task-flow-registry.types.js";
|
|
import { createTaskRecord as createTaskRecordOrNull } from "../tasks/task-registry.js";
|
|
import type { TaskRecord } from "../tasks/task-registry.types.js";
|
|
import {
|
|
configureTaskFlowRegistryRuntime,
|
|
resetTaskFlowRegistryForTests,
|
|
resetTaskRegistryDeliveryRuntimeForTests,
|
|
resetTaskRegistryForTests,
|
|
} from "../tasks/task-runtime.test-helpers.js";
|
|
import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js";
|
|
import { tasksAuditJsonCommand, tasksListJsonCommand } from "./tasks-json.js";
|
|
|
|
function createRuntime(): RuntimeEnv {
|
|
return {
|
|
log: vi.fn(),
|
|
error: vi.fn(),
|
|
exit: vi.fn(),
|
|
};
|
|
}
|
|
|
|
function createTaskRecord(params: Parameters<typeof createTaskRecordOrNull>[0]): TaskRecord {
|
|
const task = createTaskRecordOrNull(params);
|
|
if (!task) {
|
|
throw new Error("expected task creation to succeed");
|
|
}
|
|
return task;
|
|
}
|
|
|
|
function createManagedTaskFlow(
|
|
params: Parameters<typeof createManagedTaskFlowOrNull>[0],
|
|
): TaskFlowRecord {
|
|
const flow = createManagedTaskFlowOrNull(params);
|
|
if (!flow) {
|
|
throw new Error("expected managed TaskFlow creation to succeed");
|
|
}
|
|
return flow;
|
|
}
|
|
|
|
function readJsonLog(runtime: RuntimeEnv): unknown {
|
|
const [call] = vi.mocked(runtime.log).mock.calls;
|
|
if (!call) {
|
|
throw new Error("expected runtime log call");
|
|
}
|
|
return JSON.parse(String(call[0]));
|
|
}
|
|
|
|
function jsonRoundTrip<T>(value: T): T {
|
|
const serialized = JSON.stringify(value);
|
|
return JSON.parse(serialized) as T;
|
|
}
|
|
|
|
async function withTaskJsonStateDir(run: () => Promise<void>): Promise<void> {
|
|
await withOpenClawTestState(
|
|
{ layout: "state-only", prefix: "openclaw-tasks-json-command-" },
|
|
async () => {
|
|
resetTaskRegistryDeliveryRuntimeForTests();
|
|
resetTaskRegistryForTests({ persist: false });
|
|
resetTaskFlowRegistryForTests({ persist: false });
|
|
try {
|
|
await run();
|
|
} finally {
|
|
resetTaskRegistryDeliveryRuntimeForTests();
|
|
resetTaskRegistryForTests({ persist: false });
|
|
resetTaskFlowRegistryForTests({ persist: false });
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
describe("tasks JSON commands", () => {
|
|
beforeEach(() => {
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers();
|
|
resetTaskRegistryDeliveryRuntimeForTests();
|
|
resetTaskRegistryForTests({ persist: false });
|
|
resetTaskFlowRegistryForTests({ persist: false });
|
|
});
|
|
|
|
it("lists task records with runtime and status filters", async () => {
|
|
await withTaskJsonStateDir(async () => {
|
|
const cliTask = createTaskRecord({
|
|
runtime: "cli",
|
|
ownerKey: "agent:main:main",
|
|
scopeKind: "session",
|
|
runId: "run-cli",
|
|
status: "running",
|
|
task: "Inspect issue backlog",
|
|
});
|
|
createTaskRecord({
|
|
runtime: "cron",
|
|
ownerKey: "agent:main:main",
|
|
scopeKind: "session",
|
|
runId: "run-cron",
|
|
status: "queued",
|
|
task: "Refresh schedule",
|
|
});
|
|
|
|
const runtime = createRuntime();
|
|
await tasksListJsonCommand({ json: true, runtime: "cli", status: "running" }, runtime);
|
|
|
|
expect(readJsonLog(runtime)).toStrictEqual({
|
|
count: 1,
|
|
runtime: "cli",
|
|
status: "running",
|
|
tasks: [jsonRoundTrip(cliTask)],
|
|
});
|
|
});
|
|
});
|
|
|
|
it("reports blank list filters as absent in JSON output", async () => {
|
|
await withTaskJsonStateDir(async () => {
|
|
const task = createTaskRecord({
|
|
runtime: "cli",
|
|
ownerKey: "agent:main:main",
|
|
scopeKind: "session",
|
|
runId: "run-cli",
|
|
status: "running",
|
|
task: "Inspect issue backlog",
|
|
});
|
|
|
|
const runtime = createRuntime();
|
|
await tasksListJsonCommand({ json: true, runtime: " ", status: "\t" }, runtime);
|
|
|
|
expect(readJsonLog(runtime)).toStrictEqual({
|
|
count: 1,
|
|
runtime: null,
|
|
status: null,
|
|
tasks: [jsonRoundTrip(task)],
|
|
});
|
|
});
|
|
});
|
|
|
|
it("keeps audit JSON shape and combined task-flow sorting", async () => {
|
|
await withTaskJsonStateDir(async () => {
|
|
const now = Date.now();
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(now - 40 * 60_000);
|
|
createTaskRecord({
|
|
runtime: "cli",
|
|
ownerKey: "agent:main:main",
|
|
scopeKind: "session",
|
|
runId: "task-stale-running",
|
|
status: "running",
|
|
task: "Inspect issue backlog",
|
|
});
|
|
vi.setSystemTime(now);
|
|
const runningFlow = createManagedTaskFlow({
|
|
ownerKey: "agent:main:main",
|
|
controllerId: "tests/tasks-json-command",
|
|
goal: "Running flow",
|
|
status: "running",
|
|
createdAt: now - 45 * 60_000,
|
|
updatedAt: now - 45 * 60_000,
|
|
});
|
|
createManagedTaskFlow({
|
|
ownerKey: "agent:main:main",
|
|
controllerId: "tests/tasks-json-command",
|
|
goal: "Waiting flow",
|
|
status: "waiting",
|
|
createdAt: now - 40 * 60_000,
|
|
updatedAt: now - 40 * 60_000,
|
|
});
|
|
|
|
const runtime = createRuntime();
|
|
await tasksAuditJsonCommand({ json: true, limit: 1 }, runtime);
|
|
|
|
expect(readJsonLog(runtime)).toStrictEqual({
|
|
count: 5,
|
|
filteredCount: 5,
|
|
displayed: 1,
|
|
filters: {
|
|
severity: null,
|
|
code: null,
|
|
limit: 1,
|
|
},
|
|
summary: {
|
|
total: 1,
|
|
warnings: 0,
|
|
errors: 1,
|
|
byCode: {
|
|
stale_queued: 0,
|
|
stale_running: 1,
|
|
lost: 0,
|
|
delivery_failed: 0,
|
|
missing_cleanup: 0,
|
|
inconsistent_timestamps: 0,
|
|
},
|
|
taskFlows: {
|
|
total: 4,
|
|
warnings: 2,
|
|
errors: 2,
|
|
byCode: {
|
|
restore_failed: 0,
|
|
stale_running: 1,
|
|
stale_waiting: 1,
|
|
stale_blocked: 0,
|
|
cancel_stuck: 0,
|
|
missing_linked_tasks: 2,
|
|
blocked_task_missing: 0,
|
|
inconsistent_timestamps: 0,
|
|
},
|
|
},
|
|
combined: { total: 5, errors: 3, warnings: 2 },
|
|
},
|
|
findings: [
|
|
{
|
|
kind: "task_flow",
|
|
severity: "error",
|
|
code: "stale_running",
|
|
detail: "running TaskFlow has not advanced recently",
|
|
ageMs: 45 * 60_000,
|
|
status: "running",
|
|
token: runningFlow.flowId,
|
|
flow: jsonRoundTrip(runningFlow),
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|
|
|
|
it("reports blank audit filters as absent in JSON output", async () => {
|
|
await withTaskJsonStateDir(async () => {
|
|
const runtime = createRuntime();
|
|
await tasksAuditJsonCommand({ json: true, severity: " ", code: "\t" }, runtime);
|
|
|
|
expect(readJsonLog(runtime)).toMatchObject({
|
|
filters: {
|
|
severity: null,
|
|
code: null,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
it("keeps task-flow restore failures inspectable in audit JSON", async () => {
|
|
await withTaskJsonStateDir(async () => {
|
|
const loadSnapshot = vi.fn(() => {
|
|
throw new Error("SQLITE_IOERR: task-flow audit restore failed");
|
|
});
|
|
configureTaskFlowRegistryRuntime({
|
|
store: {
|
|
loadSnapshot,
|
|
saveSnapshot: () => {},
|
|
},
|
|
});
|
|
const runtime = createRuntime();
|
|
|
|
await tasksAuditJsonCommand({ json: true }, runtime);
|
|
|
|
expect(readJsonLog(runtime)).toMatchObject({
|
|
count: 1,
|
|
summary: {
|
|
taskFlows: {
|
|
total: 1,
|
|
errors: 1,
|
|
byCode: {
|
|
restore_failed: 1,
|
|
},
|
|
},
|
|
},
|
|
findings: [
|
|
{
|
|
kind: "task_flow",
|
|
severity: "error",
|
|
code: "restore_failed",
|
|
detail:
|
|
"task-flow registry restore failed: SQLITE_IOERR: task-flow audit restore failed",
|
|
},
|
|
],
|
|
});
|
|
expect(loadSnapshot).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|
|
});
|