Files
openclaw/src/cli/command-startup-policy.test.ts
hcl a97fe41a9e perf(cli): skip plugin load on agents list --json (#71739) (#71746)
Reporter measured `agents list --json` at ~7-9s on a fast host (~11s in
container) on 2026.4.23, while peer `--json` commands like
`channels list`, `cron list --all`, and `sessions ... --all-agents`
stay sub-second. Their cold-call dashboard endpoint dropped from 27s to
~2s after a local dist patch — they could even retire the 5-min cache
TTL workaround they had shipped to dodge it.

Root cause: `agents list` inherits `loadPlugins: 'always'` from the
parent `agents` policy in command-catalog, then `agentsListCommand`
calls `buildProviderStatusIndex(cfg)` unconditionally — both paths
trigger the bundled-extension import waterfall (~60+ extension index.js
modules).

`channels list` already uses `loadPlugins: 'never'` and proves the
shape is right; this PR matches that shape with the safer `text-only`
variant so human invocations are unchanged.

Two-line fix per reporter:

1. `src/cli/command-catalog.ts` — opt agents list into `text-only`,
   the same plugin-preload policy bucket that already exists. Plugin
   preload runs for human text output, skips for `--json`.

2. `src/commands/agents.commands.list.ts` — skip
   `buildProviderStatusIndex` (and the per-summary provider
   enrichment loop) when `opts.json`. Provider info is only rendered
   in human text output via `formatSummary`, so dropping it from JSON
   has no observable effect on existing callers that consume `id`,
   `name`, `model`, `bindings`, `isDefault`, `identity*`, `workspace`,
   or `agentDir`. `routes` is config-derived and continues to be set
   in both modes.

Tests:
- new assertion in command-startup-policy.test.ts: `agents list` with
  jsonOutputMode:true now resolves to `loadPlugins: false` (was
  effectively `true` via the parent `agents` 'always' policy).
- existing assertion that human (jsonOutputMode:false) still triggers
  plugin load is preserved verbatim.

6/6 tests pass. Lint clean.

Out of scope:
- `--bindings` flag opt-in for restoring providers in JSON output:
  worth adding later if any consumer needs it; reporter said dashboard
  consumers don't.
- Broader plugin-discovery cache work (#67040, #71690) which addresses
  the same family of cold-start cost.
2026-04-25 17:07:42 -04:00

142 lines
4.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
resolveCliStartupPolicy,
shouldBypassConfigGuardForCommandPath,
shouldEnsureCliPathForCommandPath,
shouldHideCliBannerForCommandPath,
shouldLoadPluginsForCommandPath,
shouldSkipRouteConfigGuardForCommandPath,
} from "./command-startup-policy.js";
describe("command-startup-policy", () => {
it("matches config guard bypass commands", () => {
expect(shouldBypassConfigGuardForCommandPath(["backup", "create"])).toBe(true);
expect(shouldBypassConfigGuardForCommandPath(["config", "validate"])).toBe(true);
expect(shouldBypassConfigGuardForCommandPath(["config", "schema"])).toBe(true);
expect(shouldBypassConfigGuardForCommandPath(["status"])).toBe(false);
});
it("matches route-first config guard skip policy", () => {
expect(
shouldSkipRouteConfigGuardForCommandPath({
commandPath: ["status"],
suppressDoctorStdout: true,
}),
).toBe(true);
expect(
shouldSkipRouteConfigGuardForCommandPath({
commandPath: ["gateway", "status"],
suppressDoctorStdout: false,
}),
).toBe(true);
expect(
shouldSkipRouteConfigGuardForCommandPath({
commandPath: ["status"],
suppressDoctorStdout: false,
}),
).toBe(false);
});
it("matches plugin preload policy", () => {
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["status"],
jsonOutputMode: false,
}),
).toBe(false);
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["status"],
jsonOutputMode: true,
}),
).toBe(false);
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["health"],
jsonOutputMode: false,
}),
).toBe(false);
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["channels", "status"],
jsonOutputMode: false,
}),
).toBe(false);
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["channels", "list"],
jsonOutputMode: false,
}),
).toBe(false);
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["channels", "add"],
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)
expect(
shouldLoadPluginsForCommandPath({
commandPath: ["agents", "list"],
jsonOutputMode: true,
}),
).toBe(false);
});
it("matches banner suppression policy", () => {
expect(shouldHideCliBannerForCommandPath(["update", "status"])).toBe(true);
expect(shouldHideCliBannerForCommandPath(["completion"])).toBe(true);
expect(
shouldHideCliBannerForCommandPath(["status"], {
...process.env,
OPENCLAW_HIDE_BANNER: "1",
}),
).toBe(true);
expect(shouldHideCliBannerForCommandPath(["status"], {})).toBe(false);
});
it("matches CLI PATH bootstrap policy", () => {
expect(shouldEnsureCliPathForCommandPath(["status"])).toBe(false);
expect(shouldEnsureCliPathForCommandPath(["sessions"])).toBe(false);
expect(shouldEnsureCliPathForCommandPath(["config", "get"])).toBe(false);
expect(shouldEnsureCliPathForCommandPath(["models", "status"])).toBe(false);
expect(shouldEnsureCliPathForCommandPath(["message", "send"])).toBe(true);
expect(shouldEnsureCliPathForCommandPath([])).toBe(true);
});
it("aggregates startup policy for commander and route-first callers", () => {
expect(
resolveCliStartupPolicy({
commandPath: ["status"],
jsonOutputMode: true,
}),
).toEqual({
suppressDoctorStdout: true,
hideBanner: false,
skipConfigGuard: false,
loadPlugins: false,
});
expect(
resolveCliStartupPolicy({
commandPath: ["status"],
jsonOutputMode: true,
routeMode: true,
}),
).toEqual({
suppressDoctorStdout: true,
hideBanner: false,
skipConfigGuard: true,
loadPlugins: false,
});
});
});