mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 20:00:42 +00:00
refactor: split manifest command alias helpers
This commit is contained in:
@@ -71,12 +71,12 @@ function buildDiscordCleanupHooks(onDelete: (key: string | undefined) => void) {
|
||||
};
|
||||
}
|
||||
|
||||
const waitFor = async (predicate: () => boolean, timeoutMs = 3_000) => {
|
||||
const waitFor = async (label: string, predicate: () => boolean, timeoutMs = 30_000) => {
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(predicate()).toBe(true);
|
||||
expect(predicate(), label).toBe(true);
|
||||
},
|
||||
{ timeout: timeoutMs, interval: 8 },
|
||||
{ timeout: timeoutMs, interval: 20 },
|
||||
);
|
||||
};
|
||||
|
||||
@@ -132,7 +132,7 @@ async function emitLifecycleEndAndFlush(params: {
|
||||
}
|
||||
|
||||
async function waitForRunCleanup(childSessionKey: string) {
|
||||
await waitFor(() => {
|
||||
await waitFor("run cleanup bookkeeping", () => {
|
||||
const run = getLatestSubagentRunByChildSessionKey(childSessionKey);
|
||||
return run?.cleanupCompletedAt != null;
|
||||
});
|
||||
@@ -213,6 +213,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
throw new Error("missing child runId");
|
||||
}
|
||||
await waitFor(
|
||||
"subagent wait, label patch, and main agent trigger",
|
||||
() =>
|
||||
ctx.waitCalls.some((call) => call.runId === child.runId) &&
|
||||
patchCalls.some((call) => call.label === "my-task") &&
|
||||
@@ -275,6 +276,7 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
"lifecycle cleanup",
|
||||
() => ctx.calls.filter((call) => call.method === "agent").length >= 2 && Boolean(deletedKey),
|
||||
);
|
||||
|
||||
@@ -336,12 +338,14 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
if (!child.runId) {
|
||||
throw new Error("missing child runId");
|
||||
}
|
||||
await waitFor(
|
||||
() =>
|
||||
ctx.waitCalls.some((call) => call.runId === child.runId) &&
|
||||
ctx.calls.filter((call) => call.method === "agent").length >= 2 &&
|
||||
Boolean(deletedKey),
|
||||
await waitFor("agent.wait called for child run", () =>
|
||||
ctx.waitCalls.some((call) => call.runId === child.runId),
|
||||
);
|
||||
await waitFor(
|
||||
"main agent cleanup trigger",
|
||||
() => ctx.calls.filter((call) => call.method === "agent").length >= 2,
|
||||
);
|
||||
await waitFor("delete cleanup", () => Boolean(deletedKey));
|
||||
|
||||
const childWait = ctx.waitCalls.find((call) => call.runId === child.runId);
|
||||
expect(childWait?.timeoutMs).toBe(1000);
|
||||
@@ -392,12 +396,13 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
}
|
||||
const childSessionKey = child.sessionKey;
|
||||
|
||||
await waitFor(() => {
|
||||
return (
|
||||
await waitFor(
|
||||
"timeout outcome",
|
||||
() =>
|
||||
ctx.waitCalls.some((call) => call.runId === child.runId) &&
|
||||
getLatestSubagentRunByChildSessionKey(childSessionKey)?.outcome?.status === "timeout"
|
||||
);
|
||||
}, 20_000);
|
||||
getLatestSubagentRunByChildSessionKey(childSessionKey)?.outcome?.status === "timeout",
|
||||
20_000,
|
||||
);
|
||||
await waitForRunCleanup(childSessionKey);
|
||||
|
||||
const childWait = ctx.waitCalls.find((call) => call.runId === child.runId);
|
||||
@@ -427,7 +432,16 @@ describe("openclaw-tools: subagents (sessions_spawn lifecycle)", () => {
|
||||
if (!child.sessionKey) {
|
||||
throw new Error("missing child sessionKey");
|
||||
}
|
||||
await waitFor(() => ctx.calls.filter((call) => call.method === "agent").length >= 2);
|
||||
await emitLifecycleEndAndFlush({
|
||||
runId: child.runId,
|
||||
startedAt: 1000,
|
||||
endedAt: 2000,
|
||||
});
|
||||
|
||||
await waitFor(
|
||||
"account-aware lifecycle announce",
|
||||
() => ctx.calls.filter((call) => call.method === "agent").length >= 2,
|
||||
);
|
||||
await waitForRunCleanup(child.sessionKey);
|
||||
|
||||
const agentCalls = ctx.calls.filter((call) => call.method === "agent");
|
||||
|
||||
@@ -11,7 +11,7 @@ import { isMainModule } from "../infra/is-main.js";
|
||||
import { ensureOpenClawCliOnPath } from "../infra/path-env.js";
|
||||
import { assertSupportedRuntime } from "../infra/runtime-guard.js";
|
||||
import { enableConsoleCapture } from "../logging.js";
|
||||
import { resolveManifestCommandAliasOwner } from "../plugins/manifest-registry.js";
|
||||
import { resolveManifestCommandAliasOwner } from "../plugins/manifest-command-aliases.runtime.js";
|
||||
import { hasMemoryRuntime } from "../plugins/memory-state.js";
|
||||
import {
|
||||
normalizeLowercaseStringOrEmpty,
|
||||
|
||||
@@ -12,6 +12,10 @@ vi.mock("../plugin-sdk/telegram-command-config.js", () => ({
|
||||
resolveTelegramCustomCommands: () => ({ commands: [], issues: [] }),
|
||||
}));
|
||||
|
||||
vi.mock("../plugins/manifest-command-aliases.runtime.js", () => ({
|
||||
resolveManifestCommandAliasOwner: () => undefined,
|
||||
}));
|
||||
|
||||
const getScopedWebSearchCredential = (key: string) => (search?: Record<string, unknown>) =>
|
||||
(search?.[key] as { apiKey?: unknown } | undefined)?.apiKey;
|
||||
const getConfiguredPluginWebSearchConfig =
|
||||
@@ -218,7 +222,6 @@ vi.mock("../plugins/manifest-registry.js", () => {
|
||||
params?.contract === "webSearchProviders"
|
||||
? mockWebSearchProviders.find((provider) => provider.id === params.value)?.pluginId
|
||||
: undefined,
|
||||
resolveManifestCommandAliasOwner: () => undefined,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
collectRelevantDoctorPluginIds,
|
||||
listPluginDoctorLegacyConfigRules,
|
||||
} from "../plugins/doctor-contract-registry.js";
|
||||
import { resolveManifestCommandAliasOwner } from "../plugins/manifest-command-aliases.runtime.js";
|
||||
import {
|
||||
loadPluginManifestRegistry,
|
||||
resolveManifestCommandAliasOwner,
|
||||
resolveManifestContractPluginIds,
|
||||
} from "../plugins/manifest-registry.js";
|
||||
import { validateJsonSchemaValue } from "../plugins/schema-validator.js";
|
||||
|
||||
26
src/plugins/manifest-command-aliases.runtime.ts
Normal file
26
src/plugins/manifest-command-aliases.runtime.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
resolveManifestCommandAliasOwnerInRegistry,
|
||||
type PluginManifestCommandAliasRecord,
|
||||
} from "./manifest-command-aliases.js";
|
||||
import { loadPluginManifestRegistry, type PluginManifestRegistry } from "./manifest-registry.js";
|
||||
|
||||
export function resolveManifestCommandAliasOwner(params: {
|
||||
command: string | undefined;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
registry?: PluginManifestRegistry;
|
||||
}): PluginManifestCommandAliasRecord | undefined {
|
||||
const registry =
|
||||
params.registry ??
|
||||
loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
return resolveManifestCommandAliasOwnerInRegistry({
|
||||
command: params.command,
|
||||
registry,
|
||||
});
|
||||
}
|
||||
47
src/plugins/manifest-command-aliases.test.ts
Normal file
47
src/plugins/manifest-command-aliases.test.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
normalizeManifestCommandAliases,
|
||||
resolveManifestCommandAliasOwnerInRegistry,
|
||||
} from "./manifest-command-aliases.js";
|
||||
|
||||
describe("manifest command aliases", () => {
|
||||
it("normalizes string and object entries", () => {
|
||||
expect(
|
||||
normalizeManifestCommandAliases([
|
||||
"memory",
|
||||
{ name: "reindex", kind: "runtime-slash", cliCommand: "memory" },
|
||||
{ name: "" },
|
||||
{ name: "bad-kind", kind: "unknown" },
|
||||
]),
|
||||
).toEqual([
|
||||
{ name: "memory" },
|
||||
{ name: "reindex", kind: "runtime-slash", cliCommand: "memory" },
|
||||
{ name: "bad-kind" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves aliases without treating plugin ids as command aliases", () => {
|
||||
const registry = {
|
||||
plugins: [
|
||||
{
|
||||
id: "memory-core",
|
||||
commandAliases: [{ name: "memory", kind: "runtime-slash" as const }],
|
||||
},
|
||||
{
|
||||
id: "memory",
|
||||
commandAliases: [{ name: "legacy-memory" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(resolveManifestCommandAliasOwnerInRegistry({ command: "memory", registry })).toBe(
|
||||
undefined,
|
||||
);
|
||||
expect(
|
||||
resolveManifestCommandAliasOwnerInRegistry({ command: "legacy-memory", registry }),
|
||||
).toMatchObject({
|
||||
pluginId: "memory",
|
||||
name: "legacy-memory",
|
||||
});
|
||||
});
|
||||
});
|
||||
88
src/plugins/manifest-command-aliases.ts
Normal file
88
src/plugins/manifest-command-aliases.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "../shared/string-coerce.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
|
||||
export type PluginManifestCommandAliasKind = "runtime-slash";
|
||||
|
||||
export type PluginManifestCommandAlias = {
|
||||
/** Command-like name users may put in plugin config by mistake. */
|
||||
name: string;
|
||||
/** Command family, used for targeted diagnostics. */
|
||||
kind?: PluginManifestCommandAliasKind;
|
||||
/** Optional root CLI command that handles related CLI operations. */
|
||||
cliCommand?: string;
|
||||
};
|
||||
|
||||
export type PluginManifestCommandAliasRecord = PluginManifestCommandAlias & {
|
||||
pluginId: string;
|
||||
};
|
||||
|
||||
export type PluginManifestCommandAliasRegistry = {
|
||||
plugins: readonly {
|
||||
id: string;
|
||||
commandAliases?: readonly PluginManifestCommandAlias[];
|
||||
}[];
|
||||
};
|
||||
|
||||
export function normalizeManifestCommandAliases(
|
||||
value: unknown,
|
||||
): PluginManifestCommandAlias[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized: PluginManifestCommandAlias[] = [];
|
||||
for (const entry of value) {
|
||||
if (typeof entry === "string") {
|
||||
const name = normalizeOptionalString(entry) ?? "";
|
||||
if (name) {
|
||||
normalized.push({ name });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!isRecord(entry)) {
|
||||
continue;
|
||||
}
|
||||
const name = normalizeOptionalString(entry.name) ?? "";
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const kind = entry.kind === "runtime-slash" ? entry.kind : undefined;
|
||||
const cliCommand = normalizeOptionalString(entry.cliCommand) ?? "";
|
||||
normalized.push({
|
||||
name,
|
||||
...(kind ? { kind } : {}),
|
||||
...(cliCommand ? { cliCommand } : {}),
|
||||
});
|
||||
}
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
export function resolveManifestCommandAliasOwnerInRegistry(params: {
|
||||
command: string | undefined;
|
||||
registry: PluginManifestCommandAliasRegistry;
|
||||
}): PluginManifestCommandAliasRecord | undefined {
|
||||
const normalizedCommand = normalizeOptionalLowercaseString(params.command);
|
||||
if (!normalizedCommand) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const commandIsPluginId = params.registry.plugins.some(
|
||||
(plugin) => normalizeOptionalLowercaseString(plugin.id) === normalizedCommand,
|
||||
);
|
||||
if (commandIsPluginId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const plugin of params.registry.plugins) {
|
||||
const alias = plugin.commandAliases?.find(
|
||||
(entry) => normalizeOptionalLowercaseString(entry.name) === normalizedCommand,
|
||||
);
|
||||
if (alias) {
|
||||
return { ...alias, pluginId: plugin.id };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
@@ -14,10 +14,10 @@ import {
|
||||
type NormalizedPluginsConfig,
|
||||
} from "./config-policy.js";
|
||||
import { discoverOpenClawPlugins, type PluginCandidate } from "./discovery.js";
|
||||
import type { PluginManifestCommandAlias } from "./manifest-command-aliases.js";
|
||||
import {
|
||||
loadPluginManifest,
|
||||
type OpenClawPackageManifest,
|
||||
type PluginManifestCommandAlias,
|
||||
type PluginManifestConfigContracts,
|
||||
type PluginManifest,
|
||||
type PluginManifestChannelConfig,
|
||||
@@ -206,47 +206,6 @@ export function resolveManifestContractOwnerPluginId(params: {
|
||||
)?.id;
|
||||
}
|
||||
|
||||
export type PluginManifestCommandAliasRecord = PluginManifestCommandAlias & {
|
||||
pluginId: string;
|
||||
};
|
||||
|
||||
export function resolveManifestCommandAliasOwner(params: {
|
||||
command: string | undefined;
|
||||
config?: OpenClawConfig;
|
||||
workspaceDir?: string;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
registry?: PluginManifestRegistry;
|
||||
}): PluginManifestCommandAliasRecord | undefined {
|
||||
const normalizedCommand = normalizeOptionalLowercaseString(params.command);
|
||||
if (!normalizedCommand) {
|
||||
return undefined;
|
||||
}
|
||||
const registry =
|
||||
params.registry ??
|
||||
loadPluginManifestRegistry({
|
||||
config: params.config,
|
||||
workspaceDir: params.workspaceDir,
|
||||
env: params.env,
|
||||
});
|
||||
|
||||
const commandIsPluginId = registry.plugins.some(
|
||||
(plugin) => normalizeOptionalLowercaseString(plugin.id) === normalizedCommand,
|
||||
);
|
||||
if (commandIsPluginId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
for (const plugin of registry.plugins) {
|
||||
const alias = plugin.commandAliases?.find(
|
||||
(entry) => normalizeOptionalLowercaseString(entry.name) === normalizedCommand,
|
||||
);
|
||||
if (alias) {
|
||||
return { ...alias, pluginId: plugin.id };
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function resolveManifestCacheMs(env: NodeJS.ProcessEnv): number {
|
||||
const raw = env.OPENCLAW_PLUGIN_MANIFEST_CACHE_MS?.trim();
|
||||
if (raw === "" || raw === "0") {
|
||||
|
||||
@@ -7,6 +7,10 @@ import { matchBoundaryFileOpenFailure, openBoundaryFileSync } from "../infra/bou
|
||||
import { normalizeOptionalString } from "../shared/string-coerce.js";
|
||||
import { normalizeTrimmedStringList } from "../shared/string-normalization.js";
|
||||
import { isRecord } from "../utils.js";
|
||||
import {
|
||||
normalizeManifestCommandAliases,
|
||||
type PluginManifestCommandAlias,
|
||||
} from "./manifest-command-aliases.js";
|
||||
import type { PluginConfigUiHint, PluginKind } from "./types.js";
|
||||
|
||||
export const PLUGIN_MANIFEST_FILENAME = "openclaw.plugin.json";
|
||||
@@ -34,17 +38,6 @@ export type PluginManifestModelSupport = {
|
||||
modelPatterns?: string[];
|
||||
};
|
||||
|
||||
export type PluginManifestCommandAliasKind = "runtime-slash";
|
||||
|
||||
export type PluginManifestCommandAlias = {
|
||||
/** Command-like name users may put in plugin config by mistake. */
|
||||
name: string;
|
||||
/** Command family, used for targeted diagnostics. */
|
||||
kind?: PluginManifestCommandAliasKind;
|
||||
/** Optional root CLI command that handles related CLI operations. */
|
||||
cliCommand?: string;
|
||||
};
|
||||
|
||||
export type PluginManifestConfigLiteral = string | number | boolean | null;
|
||||
|
||||
export type PluginManifestDangerousConfigFlag = {
|
||||
@@ -373,38 +366,6 @@ function normalizeManifestModelSupport(value: unknown): PluginManifestModelSuppo
|
||||
return Object.keys(modelSupport).length > 0 ? modelSupport : undefined;
|
||||
}
|
||||
|
||||
function normalizeManifestCommandAliases(value: unknown): PluginManifestCommandAlias[] | undefined {
|
||||
if (!Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const normalized: PluginManifestCommandAlias[] = [];
|
||||
for (const entry of value) {
|
||||
if (typeof entry === "string") {
|
||||
const name = normalizeOptionalString(entry) ?? "";
|
||||
if (name) {
|
||||
normalized.push({ name });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (!isRecord(entry)) {
|
||||
continue;
|
||||
}
|
||||
const name = normalizeOptionalString(entry.name) ?? "";
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const kind = entry.kind === "runtime-slash" ? entry.kind : undefined;
|
||||
const cliCommand = normalizeOptionalString(entry.cliCommand) ?? "";
|
||||
normalized.push({
|
||||
name,
|
||||
...(kind ? { kind } : {}),
|
||||
...(cliCommand ? { cliCommand } : {}),
|
||||
});
|
||||
}
|
||||
return normalized.length > 0 ? normalized : undefined;
|
||||
}
|
||||
|
||||
function normalizeProviderAuthChoices(
|
||||
value: unknown,
|
||||
): PluginManifestProviderAuthChoice[] | undefined {
|
||||
|
||||
46
src/plugins/setup-registry.runtime.test.ts
Normal file
46
src/plugins/setup-registry.runtime.test.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const loadPluginManifestRegistryMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./manifest-registry.js", () => ({
|
||||
loadPluginManifestRegistry: loadPluginManifestRegistryMock,
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
loadPluginManifestRegistryMock.mockReset();
|
||||
});
|
||||
|
||||
describe("setup-registry runtime fallback", () => {
|
||||
it("uses bundled manifest cliBackends when the runtime registry has no match", async () => {
|
||||
loadPluginManifestRegistryMock.mockReturnValue({
|
||||
diagnostics: [],
|
||||
plugins: [
|
||||
{
|
||||
id: "openai",
|
||||
origin: "bundled",
|
||||
cliBackends: ["Codex-CLI"],
|
||||
},
|
||||
{
|
||||
id: "local",
|
||||
origin: "workspace",
|
||||
cliBackends: ["local-cli"],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const { __testing, resolvePluginSetupCliBackendRuntime } =
|
||||
await import("./setup-registry.runtime.js");
|
||||
__testing.resetRuntimeState();
|
||||
__testing.setRuntimeModuleForTest({
|
||||
resolvePluginSetupCliBackend: () => undefined,
|
||||
});
|
||||
|
||||
expect(resolvePluginSetupCliBackendRuntime({ backend: "codex-cli" })).toEqual({
|
||||
pluginId: "openai",
|
||||
backend: { id: "Codex-CLI" },
|
||||
});
|
||||
expect(resolvePluginSetupCliBackendRuntime({ backend: "local-cli" })).toBeUndefined();
|
||||
expect(loadPluginManifestRegistryMock).toHaveBeenCalledTimes(1);
|
||||
expect(loadPluginManifestRegistryMock).toHaveBeenCalledWith({ cache: true });
|
||||
});
|
||||
});
|
||||
@@ -20,6 +20,16 @@ const SETUP_REGISTRY_RUNTIME_CANDIDATES = ["./setup-registry.js", "./setup-regis
|
||||
let setupRegistryRuntimeModule: SetupRegistryRuntimeModule | undefined;
|
||||
let bundledSetupCliBackendsCache: SetupCliBackendRuntimeEntry[] | undefined;
|
||||
|
||||
export const __testing = {
|
||||
resetRuntimeState(): void {
|
||||
setupRegistryRuntimeModule = undefined;
|
||||
bundledSetupCliBackendsCache = undefined;
|
||||
},
|
||||
setRuntimeModuleForTest(module: SetupRegistryRuntimeModule | undefined): void {
|
||||
setupRegistryRuntimeModule = module;
|
||||
},
|
||||
};
|
||||
|
||||
function resolveBundledSetupCliBackends(): SetupCliBackendRuntimeEntry[] {
|
||||
if (bundledSetupCliBackendsCache) {
|
||||
return bundledSetupCliBackendsCache;
|
||||
|
||||
Reference in New Issue
Block a user