mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:30:42 +00:00
Fix CLI text command hangs (#74220)
* fix(cli): keep agents list off plugin preload * docs(changelog): note cli text hang fix * test(cli): update preaction agents list expectations
This commit is contained in:
@@ -40,6 +40,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- CLI/agents/status: keep `openclaw agents`, text `agents list`, and plain text `status` on read-only metadata paths so human output no longer preloads plugin runtimes or live channel scans before printing. Fixes #74195. Thanks @NianJiuZst.
|
||||
- Media: treat legacy Word/OLE attachments with `application/msword` or `application/x-cfb` MIME as binary so printable-looking `.doc` files are not embedded into prompts as text. Fixes #54176; carries forward #54380. Thanks @andyliu.
|
||||
- Config: accept documented `browser.tabCleanup` keys in strict root config validation, so configured tab cleanup no longer fails before runtime reads it. Fixes #74577. Thanks @lonexreb and @ezdlp.
|
||||
- Cron: validate disabled job schedule edits before persisting updates, so invalid cron changes no longer partially mutate stored jobs. Fixes #74459. Thanks @yfge.
|
||||
|
||||
@@ -60,6 +60,12 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
{ commandPath: ["channels"], policy: { loadPlugins: "always" } },
|
||||
{ commandPath: ["directory"], policy: { loadPlugins: "always" } },
|
||||
{ commandPath: ["agents"], policy: { loadPlugins: "always", networkProxy: "bypass" } },
|
||||
{
|
||||
commandPath: ["agents"],
|
||||
exact: true,
|
||||
policy: { loadPlugins: "never", networkProxy: "bypass" },
|
||||
route: { id: "agents-list" },
|
||||
},
|
||||
{
|
||||
commandPath: ["agents", "bind"],
|
||||
exact: true,
|
||||
@@ -143,12 +149,9 @@ export const cliCommandCatalog: readonly CliCommandCatalogEntry[] = [
|
||||
},
|
||||
{
|
||||
commandPath: ["agents", "list"],
|
||||
// JSON callers (dashboards, monitoring scripts, IDE plugins) poll this
|
||||
// command and don't need the plugin-derived `providers` enrichment that
|
||||
// is only used in human text output. text-only skips the bundled-plugin
|
||||
// import waterfall in `--json` mode, mirroring what `channels list`
|
||||
// already does. Human (non-JSON) invocations still load plugins. (#71739)
|
||||
policy: { loadPlugins: "text-only", networkProxy: "bypass" },
|
||||
// Text and JSON output are derived from config plus read-only channel
|
||||
// metadata, so the route should not preload bundled plugin runtimes.
|
||||
policy: { loadPlugins: "never", networkProxy: "bypass" },
|
||||
route: { id: "agents-list" },
|
||||
},
|
||||
{
|
||||
|
||||
@@ -77,6 +77,8 @@ describe("command-path-policy", () => {
|
||||
});
|
||||
|
||||
for (const commandPath of [
|
||||
["agents"],
|
||||
["agents", "list"],
|
||||
["agents", "bind"],
|
||||
["agents", "bindings"],
|
||||
["agents", "unbind"],
|
||||
|
||||
@@ -113,15 +113,18 @@ describe("command-startup-policy", () => {
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["agents"],
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["agents", "list"],
|
||||
jsonOutputMode: false,
|
||||
}),
|
||||
).toBe(true);
|
||||
// text-only opts agents list out of plugin preload in --json mode so
|
||||
// dashboards/scripts that poll this command don't pay the bundled-plugin
|
||||
// import waterfall when they only consume config-derived fields. (#71739)
|
||||
).toBe(false);
|
||||
expect(
|
||||
shouldLoadPluginsForCommandPath({
|
||||
commandPath: ["agents", "list"],
|
||||
|
||||
@@ -201,7 +201,7 @@ describe("registerPreActionHooks", () => {
|
||||
await preActionHook(program, actionCommand);
|
||||
}
|
||||
|
||||
it("handles debug mode and plugin-required command preaction", async () => {
|
||||
it("handles debug mode and config-only command preaction", async () => {
|
||||
const processTitleSetSpy = vi.spyOn(process, "title", "set");
|
||||
await runPreAction({
|
||||
parseArgv: ["status"],
|
||||
@@ -229,7 +229,7 @@ describe("registerPreActionHooks", () => {
|
||||
runtime: runtimeMock,
|
||||
commandPath: ["agents", "list"],
|
||||
});
|
||||
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalledWith({ scope: "all" });
|
||||
expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled();
|
||||
processTitleSetSpy.mockRestore();
|
||||
});
|
||||
|
||||
@@ -512,19 +512,13 @@ describe("registerPreActionHooks", () => {
|
||||
expect(loggingState.forceConsoleToStderr).toBe(false);
|
||||
});
|
||||
|
||||
it("does not route logs to stderr during plugin loading without --json", async () => {
|
||||
let stderrDuringPluginLoad = false;
|
||||
ensurePluginRegistryLoadedMock.mockImplementation(() => {
|
||||
stderrDuringPluginLoad = loggingState.forceConsoleToStderr;
|
||||
});
|
||||
|
||||
it("does not preload plugins or route logs to stderr for agents list without --json", async () => {
|
||||
await runPreAction({
|
||||
parseArgv: ["agents", "list"],
|
||||
processArgv: ["node", "openclaw", "agents", "list"],
|
||||
});
|
||||
|
||||
expect(ensurePluginRegistryLoadedMock).toHaveBeenCalled();
|
||||
expect(stderrDuringPluginLoad).toBe(false);
|
||||
expect(ensurePluginRegistryLoadedMock).not.toHaveBeenCalled();
|
||||
expect(loggingState.forceConsoleToStderr).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
@@ -111,6 +111,10 @@ describe("route-args", () => {
|
||||
json: true,
|
||||
bindings: true,
|
||||
});
|
||||
expect(parseAgentsListRouteArgs(["node", "openclaw", "agents"])).toEqual({
|
||||
json: false,
|
||||
bindings: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("parses config routes", () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 () => {}));
|
||||
const agentsListCommandMock = vi.hoisted(() => vi.fn(async () => {}));
|
||||
|
||||
vi.mock("../config-cli.js", () => ({
|
||||
runConfigGet: runConfigGetMock,
|
||||
@@ -49,6 +50,10 @@ vi.mock("../../commands/channels/status.js", () => ({
|
||||
channelsStatusCommand: channelsStatusCommandMock,
|
||||
}));
|
||||
|
||||
vi.mock("../../commands/agents.js", () => ({
|
||||
agentsListCommand: agentsListCommandMock,
|
||||
}));
|
||||
|
||||
describe("program routes", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -80,6 +85,34 @@ describe("program routes", () => {
|
||||
expect(expectRoute(["channels", "status"])?.loadPlugins).toBeUndefined();
|
||||
});
|
||||
|
||||
it("matches agents read-only routes without plugin preload", () => {
|
||||
expect(expectRoute(["agents"])?.loadPlugins).toBeUndefined();
|
||||
expect(expectRoute(["agents", "list"])?.loadPlugins).toBeUndefined();
|
||||
});
|
||||
|
||||
it("passes parsed agents list flags through", async () => {
|
||||
await expect(expectRoute(["agents"])?.run(["node", "openclaw", "agents"])).resolves.toBe(true);
|
||||
expect(agentsListCommandMock).toHaveBeenCalledWith(
|
||||
{ json: false, bindings: false },
|
||||
expect.any(Object),
|
||||
);
|
||||
|
||||
await expect(
|
||||
expectRoute(["agents", "list"])?.run([
|
||||
"node",
|
||||
"openclaw",
|
||||
"agents",
|
||||
"list",
|
||||
"--json",
|
||||
"--bindings",
|
||||
]),
|
||||
).resolves.toBe(true);
|
||||
expect(agentsListCommandMock).toHaveBeenLastCalledWith(
|
||||
{ json: true, bindings: true },
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("passes parsed channel read-only route flags through", async () => {
|
||||
const listRoute = expectRoute(["channels", "list"]);
|
||||
await expect(
|
||||
|
||||
@@ -112,4 +112,17 @@ describe("agentsListCommand", () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps human output enriched from read-only provider metadata", async () => {
|
||||
const runtime = createRuntime();
|
||||
|
||||
await agentsListCommand({}, runtime);
|
||||
|
||||
expect(buildProviderStatusIndexMock).toHaveBeenCalledOnce();
|
||||
expect(buildProviderSummaryMetadataIndexMock).toHaveBeenCalledOnce();
|
||||
expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("Providers:"));
|
||||
expect(runtime.log).toHaveBeenCalledWith(
|
||||
expect.stringContaining("Telegram default: configured"),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user