fix: route tasks json through lean cli path

This commit is contained in:
Peter Steinberger
2026-04-27 12:13:33 +01:00
parent e20f755ac5
commit 78577ac147
13 changed files with 628 additions and 4 deletions

View File

@@ -261,3 +261,69 @@ export function parseChannelsStatusRouteArgs(argv: string[]) {
timeout: timeout.value,
};
}
function parseTasksListRouteArgsForCommandPath(argv: string[], commandPath: string[]) {
if (!hasFlag(argv, "--json")) {
return null;
}
const positionals = getCommandPositionalsWithRootOptions(argv, {
commandPath,
booleanFlags: ["--json"],
valueFlags: ["--runtime", "--status"],
});
if (!positionals || positionals.length !== 0) {
return null;
}
const runtime = parseOptionalFlagValue(argv, "--runtime");
if (!runtime.ok) {
return null;
}
const status = parseOptionalFlagValue(argv, "--status");
if (!status.ok) {
return null;
}
return {
json: true as const,
runtime: runtime.value,
status: status.value,
};
}
export function parseTasksListRouteArgs(argv: string[]) {
return (
parseTasksListRouteArgsForCommandPath(argv, ["tasks"]) ??
parseTasksListRouteArgsForCommandPath(argv, ["tasks", "list"])
);
}
export function parseTasksAuditRouteArgs(argv: string[]) {
if (!hasFlag(argv, "--json")) {
return null;
}
const positionals = getCommandPositionalsWithRootOptions(argv, {
commandPath: ["tasks", "audit"],
booleanFlags: ["--json"],
valueFlags: ["--severity", "--code", "--limit"],
});
if (!positionals || positionals.length !== 0) {
return null;
}
const severity = parseOptionalFlagValue(argv, "--severity");
if (!severity.ok) {
return null;
}
const code = parseOptionalFlagValue(argv, "--code");
if (!code.ok) {
return null;
}
const limit = getPositiveIntFlagValue(argv, "--limit");
if (limit === null) {
return null;
}
return {
json: true as const,
severity: severity.value,
code: code.value,
limit,
};
}

View File

@@ -9,6 +9,7 @@ import {
export type RouteSpec = {
matches: (path: string[]) => boolean;
canRun?: (argv: string[]) => boolean;
loadPlugins?: boolean | ((argv: string[]) => boolean);
run: (argv: string[]) => Promise<boolean>;
};
@@ -27,6 +28,7 @@ function createParsedRoute(params: {
return {
matches: (path) =>
matchesCommandPath(path, params.entry.commandPath, { exact: params.entry.exact }),
canRun: (argv) => Boolean(params.definition.parseArgs(argv)),
loadPlugins: params.entry.route?.preloadPlugins
? createCommandLoadPlugins(params.entry.commandPath)
: undefined,

View File

@@ -11,6 +11,8 @@ import {
parseModelsStatusRouteArgs,
parseSessionsRouteArgs,
parseStatusRouteArgs,
parseTasksAuditRouteArgs,
parseTasksListRouteArgs,
} from "./route-args.js";
type RouteArgParser<TArgs> = (argv: string[]) => TArgs | null;
@@ -132,6 +134,20 @@ export const routedCommandDefinitions = {
await modelsStatusCommand(args, defaultRuntime);
},
}),
"tasks-list": defineRoutedCommand({
parseArgs: parseTasksListRouteArgs,
runParsedArgs: async (args) => {
const { tasksListJsonCommand } = await import("../../commands/tasks-json.js");
await tasksListJsonCommand(args, defaultRuntime);
},
}),
"tasks-audit": defineRoutedCommand({
parseArgs: parseTasksAuditRouteArgs,
runParsedArgs: async (args) => {
const { tasksAuditJsonCommand } = await import("../../commands/tasks-json.js");
await tasksAuditJsonCommand(args, defaultRuntime);
},
}),
"channels-list": defineRoutedCommand({
parseArgs: parseChannelsListRouteArgs,
runParsedArgs: async (args) => {

View File

@@ -7,6 +7,8 @@ const modelsListCommandMock = vi.hoisted(() => vi.fn(async () => {}));
const modelsStatusCommandMock = vi.hoisted(() => vi.fn(async () => {}));
const runDaemonStatusMock = vi.hoisted(() => vi.fn(async () => {}));
const statusJsonCommandMock = vi.hoisted(() => vi.fn(async () => {}));
const tasksListJsonCommandMock = vi.hoisted(() => vi.fn(async () => {}));
const tasksAuditJsonCommandMock = vi.hoisted(() => vi.fn(async () => {}));
const channelsListCommandMock = vi.hoisted(() => vi.fn(async () => {}));
const channelsStatusCommandMock = vi.hoisted(() => vi.fn(async () => {}));
@@ -38,6 +40,15 @@ vi.mock("../../commands/status-json.js", () => ({
statusJsonCommand: statusJsonCommandMock,
}));
vi.mock("../../commands/tasks-json.js", () => ({
tasksListJsonCommand: tasksListJsonCommandMock,
tasksAuditJsonCommand: tasksAuditJsonCommandMock,
}));
vi.mock("../../commands/tasks.js", () => {
throw new Error("routed task JSON commands must not import the full tasks command module");
});
vi.mock("../../commands/channels/list.js", () => ({
channelsListCommand: channelsListCommandMock,
}));
@@ -376,4 +387,117 @@ describe("program routes", () => {
expect.any(Object),
);
});
it("routes tasks list JSON through the lean task JSON command", async () => {
const rootRoute = expectRoute(["tasks"]);
expect(rootRoute?.loadPlugins).toBeUndefined();
expect(rootRoute?.canRun?.(["node", "openclaw", "tasks"])).toBe(false);
await expect(
rootRoute?.run([
"node",
"openclaw",
"tasks",
"--json",
"--runtime",
"cli",
"--status=running",
]),
).resolves.toBe(true);
expect(tasksListJsonCommandMock).toHaveBeenCalledWith(
{ json: true, runtime: "cli", status: "running" },
expect.any(Object),
);
const listRoute = expectRoute(["tasks", "list"]);
expect(listRoute?.loadPlugins).toBeUndefined();
await expect(
listRoute?.run(["node", "openclaw", "tasks", "list", "--json", "--runtime=cron"]),
).resolves.toBe(true);
expect(tasksListJsonCommandMock).toHaveBeenLastCalledWith(
{ json: true, runtime: "cron", status: undefined },
expect.any(Object),
);
});
it("routes parent task filter values that command-path discovery sees as positionals", async () => {
const separateValueArgv = [
"node",
"openclaw",
"tasks",
"--json",
"--runtime",
"cli",
"--status",
"running",
];
const separateValueRoute = findRoutedCommand(["tasks", "cli"], separateValueArgv);
expect(separateValueRoute).not.toBeNull();
await expect(separateValueRoute?.run(separateValueArgv)).resolves.toBe(true);
expect(tasksListJsonCommandMock).toHaveBeenCalledWith(
{ json: true, runtime: "cli", status: "running" },
expect.any(Object),
);
const parentOptionBeforeSubcommandArgv = [
"node",
"openclaw",
"tasks",
"--runtime",
"cli",
"list",
"--json",
];
const parentOptionBeforeSubcommandRoute = findRoutedCommand(
["tasks", "cli"],
parentOptionBeforeSubcommandArgv,
);
expect(parentOptionBeforeSubcommandRoute).not.toBeNull();
await expect(
parentOptionBeforeSubcommandRoute?.run(parentOptionBeforeSubcommandArgv),
).resolves.toBe(true);
expect(tasksListJsonCommandMock).toHaveBeenLastCalledWith(
{ json: true, runtime: "cli", status: undefined },
expect.any(Object),
);
});
it("routes tasks audit JSON through the lean task JSON command", async () => {
const route = expectRoute(["tasks", "audit"]);
expect(route?.loadPlugins).toBeUndefined();
expect(route?.canRun?.(["node", "openclaw", "tasks", "audit"])).toBe(false);
await expect(
route?.run([
"node",
"openclaw",
"tasks",
"audit",
"--json",
"--severity",
"error",
"--code=stale_running",
"--limit",
"5",
]),
).resolves.toBe(true);
expect(tasksAuditJsonCommandMock).toHaveBeenCalledWith(
{ json: true, severity: "error", code: "stale_running", limit: 5 },
expect.any(Object),
);
});
it("returns false for task JSON routes when option values are missing or unknown", async () => {
await expectRunFalse(["tasks"], ["node", "openclaw", "tasks", "--json", "--runtime"]);
await expectRunFalse(["tasks", "list"], ["node", "openclaw", "tasks", "list"]);
await expectRunFalse(
["tasks", "audit"],
["node", "openclaw", "tasks", "audit", "--json", "--limit"],
);
await expectRunFalse(
["tasks", "audit"],
["node", "openclaw", "tasks", "audit", "--json", "--unknown"],
);
expect(
findRoutedCommand(["tasks", "cli"], ["node", "openclaw", "tasks", "--runtime", "cli"]),
).toBeNull();
});
});

View File

@@ -2,9 +2,12 @@ import { routedCommands, type RouteSpec } from "./route-specs.js";
export type { RouteSpec } from "./route-specs.js";
export function findRoutedCommand(path: string[]): RouteSpec | null {
export function findRoutedCommand(path: string[], argv?: string[]): RouteSpec | null {
for (const route of routedCommands) {
if (route.matches(path)) {
if (argv && route.canRun && !route.canRun(argv)) {
continue;
}
return route;
}
}