diff --git a/src/cli/program/routes.test.ts b/src/cli/program/routes.test.ts index 7c35591ca788..6960cf5fd471 100644 --- a/src/cli/program/routes.test.ts +++ b/src/cli/program/routes.test.ts @@ -520,6 +520,24 @@ describe("program routes", () => { { json: true, runtime: "cron", status: undefined }, defaultRuntime, ); + + await expect( + listRoute.run([ + "node", + "openclaw", + "tasks", + "list", + "--json", + "--runtime", + " ", + "--status", + "\t", + ]), + ).resolves.toBe(true); + expect(tasksListJsonCommandMock).toHaveBeenLastCalledWith( + { json: true, runtime: " ", status: "\t" }, + defaultRuntime, + ); }); it("routes parent task filter values that command-path discovery sees as positionals", async () => { @@ -584,6 +602,24 @@ describe("program routes", () => { { json: true, severity: "error", code: "stale_running", limit: 5 }, defaultRuntime, ); + + await expect( + route.run([ + "node", + "openclaw", + "tasks", + "audit", + "--json", + "--severity", + " ", + "--code", + "\t", + ]), + ).resolves.toBe(true); + expect(tasksAuditJsonCommandMock).toHaveBeenLastCalledWith( + { json: true, severity: " ", code: "\t", limit: undefined }, + defaultRuntime, + ); }); it("returns false for task JSON routes when option values are missing or unknown", async () => { diff --git a/src/commands/flows.test.ts b/src/commands/flows.test.ts index 428bdf8cbbcb..0f4f3f02f54c 100644 --- a/src/commands/flows.test.ts +++ b/src/commands/flows.test.ts @@ -162,6 +162,29 @@ describe("flows commands", () => { }); }); + it("reports blank status filters as absent in JSON output", async () => { + await withTaskFlowCommandStateDir(async () => { + const flow = createManagedTaskFlow({ + ownerKey: "agent:main:main", + controllerId: "tests/flows-command", + goal: "Inspect a PR cluster", + status: "running", + createdAt: 100, + updatedAt: 100, + }); + + const runtime = createRuntime(); + await flowsListCommand({ json: true, status: " " }, runtime); + + expect(runtime.log).not.toHaveBeenCalled(); + expect(jsonRoundTrip(vi.mocked(runtime.writeJson).mock.calls[0]?.[0])).toMatchObject({ + count: 1, + status: null, + flows: [expect.objectContaining(jsonRoundTrip(flow))], + }); + }); + }); + it("keeps truncated text rows UTF-16 well-formed", async () => { await withTaskFlowCommandStateDir(async () => { createManagedTaskFlow({ diff --git a/src/commands/flows.ts b/src/commands/flows.ts index 34b5e7391223..ad85553ee992 100644 --- a/src/commands/flows.ts +++ b/src/commands/flows.ts @@ -154,7 +154,7 @@ export async function flowsListCommand( opts: { json?: boolean; status?: string }, runtime: RuntimeEnv, ) { - const statusFilter = opts.status?.trim(); + const statusFilter = normalizeOptionalString(opts.status); const flows = listTaskFlowRecords().filter((flow) => { if (statusFilter && flow.status !== statusFilter) { return false; diff --git a/src/commands/tasks-json.test.ts b/src/commands/tasks-json.test.ts index d37356ca5713..8c68d016375a 100644 --- a/src/commands/tasks-json.test.ts +++ b/src/commands/tasks-json.test.ts @@ -115,6 +115,29 @@ describe("tasks JSON commands", () => { }); }); + 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(); @@ -202,4 +225,18 @@ describe("tasks JSON commands", () => { }); }); }); + + 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, + }, + }); + }); + }); }); diff --git a/src/commands/tasks-json.ts b/src/commands/tasks-json.ts index 18e6e877271b..1dc10190cae6 100644 --- a/src/commands/tasks-json.ts +++ b/src/commands/tasks-json.ts @@ -1,6 +1,7 @@ // JSON-only task command helpers. // These paths avoid maintenance reconciliation so short-lived JSON CLI processes stay read-only and exit cleanly. +import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce"; import type { RuntimeEnv } from "../runtime.js"; import { writeRuntimeJson } from "../runtime.js"; import { listTaskRecords } from "../tasks/runtime-internal.js"; @@ -52,8 +53,8 @@ function toSystemAuditFindings(params: { } function buildTasksListJsonPayload(opts: TasksListJsonArgs) { - const runtimeFilter = opts.runtime?.trim(); - const statusFilter = opts.status?.trim(); + const runtimeFilter = normalizeOptionalString(opts.runtime); + const statusFilter = normalizeOptionalString(opts.status); const tasks = listTaskJsonRecords().filter((task) => { if (runtimeFilter && task.runtime !== runtimeFilter) { return false; @@ -72,8 +73,10 @@ function buildTasksListJsonPayload(opts: TasksListJsonArgs) { } function buildTasksAuditJsonPayload(opts: TasksAuditJsonArgs) { - const severityFilter = opts.severity?.trim() as TaskSystemAuditSeverity | undefined; - const codeFilter = opts.code?.trim() as TaskSystemAuditCode | undefined; + const severityFilter = normalizeOptionalString(opts.severity) as + | TaskSystemAuditSeverity + | undefined; + const codeFilter = normalizeOptionalString(opts.code) as TaskSystemAuditCode | undefined; const result = toSystemAuditFindings({ severityFilter, codeFilter, diff --git a/src/commands/tasks.test.ts b/src/commands/tasks.test.ts index cbc5f74430d7..9e1dc24692bd 100644 --- a/src/commands/tasks.test.ts +++ b/src/commands/tasks.test.ts @@ -21,6 +21,7 @@ import * as taskRegistryMaintenance from "../tasks/task-registry.maintenance.js" import type { TaskRecord } from "../tasks/task-registry.types.js"; import { withOpenClawTestState } from "../test-utils/openclaw-test-state.js"; import type { OpenClawTestState } from "../test-utils/openclaw-test-state.js"; +import type { TaskSystemAuditCode, TaskSystemAuditSeverity } from "./tasks-audit-system.js"; import { tasksAuditCommand, tasksCancelCommand, @@ -201,6 +202,50 @@ describe("tasks commands", () => { }); }); + it("reports blank list filters as absent in command JSON output", async () => { + await withTaskCommandStateDir(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 tasksListCommand({ json: true, runtime: " ", status: "\t" }, runtime); + + expect(readFirstJsonLog(runtime)).toStrictEqual({ + count: 1, + runtime: null, + status: null, + tasks: [jsonRoundTrip(task)], + }); + }); + }); + + it("reports blank audit filters as absent in command JSON output", async () => { + await withTaskCommandStateDir(async () => { + const runtime = createRuntime(); + await tasksAuditCommand( + { + json: true, + severity: " " as TaskSystemAuditSeverity, + code: "\t" as TaskSystemAuditCode, + }, + runtime, + ); + + expect(readFirstJsonLog(runtime)).toMatchObject({ + filters: { + severity: null, + code: null, + }, + }); + }); + }); + it("routes cron task cancellation through the live gateway before local fallback", async () => { await withTaskCommandStateDir(async () => { const task = createTaskRecord({ diff --git a/src/commands/tasks.ts b/src/commands/tasks.ts index 76e45d65753f..569bf250368b 100644 --- a/src/commands/tasks.ts +++ b/src/commands/tasks.ts @@ -349,8 +349,8 @@ export async function tasksListCommand( opts: { json?: boolean; runtime?: string; status?: string }, runtime: RuntimeEnv, ) { - const runtimeFilter = opts.runtime?.trim(); - const statusFilter = opts.status?.trim(); + const runtimeFilter = normalizeOptionalString(opts.runtime); + const statusFilter = normalizeOptionalString(opts.status); const tasks = reconcileInspectableTasks().filter((task) => { if (runtimeFilter && task.runtime !== runtimeFilter) { return false; @@ -524,8 +524,10 @@ export async function tasksAuditCommand( runtime: RuntimeEnv, ) { configureTaskMaintenanceFromConfig(); - const severityFilter = opts.severity?.trim() as TaskSystemAuditSeverity | undefined; - const codeFilter = opts.code?.trim() as TaskSystemAuditCode | undefined; + const severityFilter = normalizeOptionalString(opts.severity) as + | TaskSystemAuditSeverity + | undefined; + const codeFilter = normalizeOptionalString(opts.code) as TaskSystemAuditCode | undefined; const auditResult = toSystemAuditFindings({ severityFilter, codeFilter,