mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(plugins): snapshot command registrations
This commit is contained in:
@@ -375,7 +375,7 @@ function copyAgentPromptGuidance(
|
||||
return { ok: true, value: guidance };
|
||||
}
|
||||
|
||||
function snapshotPluginCommandDefinition(
|
||||
export function snapshotPluginCommandDefinition(
|
||||
command: OpenClawPluginCommandDefinition,
|
||||
): CommandSnapshotResult {
|
||||
if (!isRecord(command)) {
|
||||
|
||||
112
src/plugins/contracts/command-registration.contract.test.ts
Normal file
112
src/plugins/contracts/command-registration.contract.test.ts
Normal file
@@ -0,0 +1,112 @@
|
||||
// Command registration tests cover plugin-owned command definition snapshotting.
|
||||
import {
|
||||
createPluginRegistryFixture,
|
||||
registerTestPlugin,
|
||||
} from "openclaw/plugin-sdk/plugin-test-contracts";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
getPluginCommandEntrySpecsFromRegistrations,
|
||||
getPluginCommandSpecsFromRegistrations,
|
||||
} from "../command-specs.js";
|
||||
import { clearPluginCommands } from "../commands.js";
|
||||
import { resetPluginRuntimeStateForTest } from "../runtime.js";
|
||||
import { createPluginRecord } from "../status.test-helpers.js";
|
||||
import type { OpenClawPluginCommandDefinition } from "../types.js";
|
||||
|
||||
describe("plugin command registration", () => {
|
||||
afterEach(() => {
|
||||
clearPluginCommands();
|
||||
resetPluginRuntimeStateForTest();
|
||||
});
|
||||
|
||||
it("snapshots command fields before registry command projection", () => {
|
||||
let nameReads = 0;
|
||||
let descriptionReads = 0;
|
||||
let nativeNamesReads = 0;
|
||||
let descriptionLocalizationsReads = 0;
|
||||
let acceptsArgsReads = 0;
|
||||
let handlerReads = 0;
|
||||
const handler: OpenClawPluginCommandDefinition["handler"] = async () => ({
|
||||
text: "ok",
|
||||
});
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
registerTestPlugin({
|
||||
registry,
|
||||
config,
|
||||
record: createPluginRecord({
|
||||
id: "volatile-command-plugin",
|
||||
name: "Volatile Command Plugin",
|
||||
}),
|
||||
register(api) {
|
||||
api.registerCommand({
|
||||
get name() {
|
||||
nameReads += 1;
|
||||
if (nameReads > 1) {
|
||||
throw new Error("command name getter re-read");
|
||||
}
|
||||
return " volatile ";
|
||||
},
|
||||
get description() {
|
||||
descriptionReads += 1;
|
||||
if (descriptionReads > 1) {
|
||||
throw new Error("command description getter re-read");
|
||||
}
|
||||
return " Stable command description. ";
|
||||
},
|
||||
get nativeNames() {
|
||||
nativeNamesReads += 1;
|
||||
if (nativeNamesReads > 1) {
|
||||
throw new Error("command nativeNames getter re-read");
|
||||
}
|
||||
return { default: "volatile-native" };
|
||||
},
|
||||
get descriptionLocalizations() {
|
||||
descriptionLocalizationsReads += 1;
|
||||
if (descriptionLocalizationsReads > 1) {
|
||||
throw new Error("command descriptionLocalizations getter re-read");
|
||||
}
|
||||
return { fr: "Commande stable" };
|
||||
},
|
||||
get acceptsArgs() {
|
||||
acceptsArgsReads += 1;
|
||||
if (acceptsArgsReads > 1) {
|
||||
throw new Error("command acceptsArgs getter re-read");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
get handler() {
|
||||
handlerReads += 1;
|
||||
if (handlerReads > 1) {
|
||||
throw new Error("command handler getter re-read");
|
||||
}
|
||||
return handler;
|
||||
},
|
||||
} as OpenClawPluginCommandDefinition);
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.registry.commands[0]?.command.name).toBe(" volatile ");
|
||||
expect(getPluginCommandEntrySpecsFromRegistrations(registry.registry.commands)).toEqual([
|
||||
{
|
||||
name: "volatile",
|
||||
description: "Stable command description.",
|
||||
acceptsArgs: true,
|
||||
nativeName: "volatile-native",
|
||||
},
|
||||
]);
|
||||
expect(getPluginCommandSpecsFromRegistrations(registry.registry.commands)).toEqual([
|
||||
{
|
||||
name: "volatile-native",
|
||||
description: "Stable command description.",
|
||||
descriptionLocalizations: { fr: "Commande stable" },
|
||||
acceptsArgs: true,
|
||||
},
|
||||
]);
|
||||
expect(nameReads).toBe(1);
|
||||
expect(descriptionReads).toBe(1);
|
||||
expect(nativeNamesReads).toBe(1);
|
||||
expect(descriptionLocalizationsReads).toBe(1);
|
||||
expect(acceptsArgsReads).toBe(1);
|
||||
expect(handlerReads).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -65,6 +65,7 @@ import type { CodexAppServerExtensionFactory } from "./codex-app-server-extensio
|
||||
import {
|
||||
isReservedCommandName,
|
||||
registerPluginCommand,
|
||||
snapshotPluginCommandDefinition,
|
||||
validatePluginCommandDefinition,
|
||||
} from "./command-registration.js";
|
||||
import { clearPluginCommandsForPlugin, pluginCommands } from "./command-registry-state.js";
|
||||
@@ -2008,7 +2009,18 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
};
|
||||
|
||||
const registerCommand = (record: PluginRecord, command: OpenClawPluginCommandDefinition) => {
|
||||
const name = command.name.trim();
|
||||
const snapshot = snapshotPluginCommandDefinition(command);
|
||||
if (!snapshot.ok) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: `command registration failed: ${snapshot.error}`,
|
||||
});
|
||||
return;
|
||||
}
|
||||
const commandSnapshot = snapshot.command;
|
||||
const name = commandSnapshot.name.trim();
|
||||
if (!name) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
@@ -2018,7 +2030,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
const allowReservedCommandNames = command.ownership === "reserved";
|
||||
const allowReservedCommandNames = commandSnapshot.ownership === "reserved";
|
||||
if (allowReservedCommandNames && !canClaimReservedCommandOwnership(record)) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
@@ -2054,7 +2066,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
// snapshot registries are isolated and never write to the global command table. Conflicts
|
||||
// will surface when the plugin is loaded via the normal activation path at gateway startup.
|
||||
if (!registryParams.activateGlobalSideEffects) {
|
||||
const validationError = validatePluginCommandDefinition(command, {
|
||||
const validationError = validatePluginCommandDefinition(commandSnapshot, {
|
||||
allowReservedCommandNames,
|
||||
});
|
||||
if (validationError) {
|
||||
@@ -2067,11 +2079,11 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const { ownership: _ownership, ...commandForRegistration } = command;
|
||||
const { ownership: _ownership, ...commandForRegistration } = commandSnapshot;
|
||||
void _ownership;
|
||||
const result = registerPluginCommand(
|
||||
record.id,
|
||||
allowReservedCommandNames ? commandForRegistration : command,
|
||||
allowReservedCommandNames ? commandForRegistration : commandSnapshot,
|
||||
{
|
||||
pluginName: record.name,
|
||||
pluginRoot: record.rootDir,
|
||||
@@ -2100,7 +2112,7 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
registry.commands.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
command,
|
||||
command: commandSnapshot,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user