mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:10:44 +00:00
fix: stabilize release validation
This commit is contained in:
@@ -22,6 +22,50 @@
|
||||
"feishu_wiki"
|
||||
]
|
||||
},
|
||||
"toolMetadata": {
|
||||
"feishu_app_scopes": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_bitable_create_app": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_bitable_create_field": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_bitable_create_record": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_bitable_get_meta": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_bitable_get_record": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_bitable_list_fields": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_bitable_list_records": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_bitable_update_record": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_chat": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_doc": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_drive": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_perm": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
},
|
||||
"feishu_wiki": {
|
||||
"configSignals": [{ "rootPath": "channels.feishu", "required": ["appId", "appSecret"] }]
|
||||
}
|
||||
},
|
||||
"channelEnvVars": {
|
||||
"feishu": [
|
||||
"FEISHU_APP_ID",
|
||||
|
||||
@@ -109,12 +109,14 @@ function buildPluginPlan(manifest) {
|
||||
? contracts.speechProviders.filter(isNonEmptyString)
|
||||
: [];
|
||||
const tools = Array.isArray(contracts.tools) ? contracts.tools.filter(isNonEmptyString) : [];
|
||||
const toolMetadata =
|
||||
manifest.toolMetadata && typeof manifest.toolMetadata === "object" ? manifest.toolMetadata : {};
|
||||
const activeInThisProbe =
|
||||
manifest.activation?.onStartup === true || channels.length > 0 || speechProviders.length > 0;
|
||||
return {
|
||||
channels,
|
||||
speechProviders,
|
||||
tools,
|
||||
tools: tools.filter((tool) => !toolMetadata[tool]),
|
||||
activeInThisProbe,
|
||||
runtimeSlashAliases: commandAliases
|
||||
.filter((alias) => alias?.kind === "runtime-slash")
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import path from "node:path";
|
||||
import { requireArg, write, writeJson } from "./common.mjs";
|
||||
|
||||
function writePluginManifest(file, id) {
|
||||
writeJson(file, { id, configSchema: { type: "object", properties: {} } });
|
||||
function writePluginManifest(file, id, extra = {}) {
|
||||
writeJson(file, { id, ...extra, configSchema: { type: "object", properties: {} } });
|
||||
}
|
||||
|
||||
function writeFakeIsNumberPackage(dir) {
|
||||
@@ -19,7 +19,9 @@ function writePluginDemo([dir]) {
|
||||
path.join(requireArg(dir, "dir"), "index.js"),
|
||||
'module.exports = { id: "demo-plugin", name: "Demo Plugin", description: "Docker E2E demo plugin", register(api) { api.registerTool(() => null, { name: "demo_tool" }); api.registerGatewayMethod("demo.ping", async () => ({ ok: true })); api.registerCli(() => {}, { commands: ["demo"] }); api.registerService({ id: "demo-service", start: () => {} }); }, };\n',
|
||||
);
|
||||
writePluginManifest(path.join(dir, "openclaw.plugin.json"), "demo-plugin");
|
||||
writePluginManifest(path.join(dir, "openclaw.plugin.json"), "demo-plugin", {
|
||||
contracts: { tools: ["demo_tool"] },
|
||||
});
|
||||
}
|
||||
|
||||
function writePlugin([dir, id, version, method, name]) {
|
||||
|
||||
@@ -152,6 +152,7 @@ function assertExpectedDiagnostics(surfaceMode, errorMessages) {
|
||||
'channel "kitchen-sink-channel-probe" registration missing required config helpers',
|
||||
"cli registration missing explicit commands metadata",
|
||||
"only bundled plugins can register Codex app-server extension factories",
|
||||
"only bundled plugins can register agent tool result middleware",
|
||||
'compaction provider "kitchen-sink-compaction-provider" registration missing summarize',
|
||||
"context engine registration missing id",
|
||||
"http route registration missing or invalid auth: /kitchen-sink/http-route",
|
||||
|
||||
@@ -301,6 +301,11 @@ async function runAgentWithSessionKey(sessionKey: string): Promise<void> {
|
||||
await agentCommand({ message: "hi", sessionKey }, runtime);
|
||||
}
|
||||
|
||||
function mockModelCatalogOnce(entries: ReturnType<typeof loadManifestModelCatalog>): void {
|
||||
vi.mocked(loadManifestModelCatalog).mockReturnValueOnce(entries);
|
||||
vi.mocked(loadModelCatalog).mockResolvedValueOnce(entries);
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
clearSessionStoreCacheForTest();
|
||||
@@ -309,6 +314,7 @@ beforeEach(() => {
|
||||
acpManagerTesting.resetAcpSessionManagerForTests();
|
||||
runtimeSnapshotModule.clearRuntimeConfigSnapshot();
|
||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue(createDefaultAgentResult());
|
||||
vi.mocked(loadManifestModelCatalog).mockReturnValue([]);
|
||||
vi.mocked(loadModelCatalog).mockResolvedValue([]);
|
||||
vi.mocked(modelSelectionModule.isCliProvider).mockImplementation(() => false);
|
||||
configIoMocks.readConfigFileSnapshotForWrite.mockResolvedValue({
|
||||
@@ -607,13 +613,11 @@ describe("agentCommand", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const catalog = [
|
||||
mockModelCatalogOnce([
|
||||
{ id: "claude-opus-4-6", name: "Opus", provider: "anthropic" },
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" },
|
||||
{ id: "gpt-5.4", name: "GPT-5.2", provider: "openai" },
|
||||
];
|
||||
vi.mocked(loadModelCatalog).mockResolvedValueOnce(catalog);
|
||||
vi.mocked(loadManifestModelCatalog).mockReturnValueOnce(catalog);
|
||||
]);
|
||||
vi.mocked(runEmbeddedPiAgent)
|
||||
.mockRejectedValueOnce(Object.assign(new Error("rate limited"), { status: 429 }))
|
||||
.mockResolvedValueOnce({
|
||||
@@ -667,13 +671,11 @@ describe("agentCommand", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const catalog = [
|
||||
mockModelCatalogOnce([
|
||||
{ id: "qwen3.5:27b", name: "Qwen 3.5", provider: "ollama" },
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" },
|
||||
{ id: "gpt-5.4", name: "GPT-5.4", provider: "openai" },
|
||||
];
|
||||
vi.mocked(loadModelCatalog).mockResolvedValueOnce(catalog);
|
||||
vi.mocked(loadManifestModelCatalog).mockReturnValueOnce(catalog);
|
||||
]);
|
||||
vi.mocked(runEmbeddedPiAgent).mockRejectedValueOnce(new Error("connect ECONNREFUSED"));
|
||||
|
||||
await expect(
|
||||
@@ -718,12 +720,10 @@ describe("agentCommand", () => {
|
||||
},
|
||||
});
|
||||
|
||||
const catalog = [
|
||||
mockModelCatalogOnce([
|
||||
{ id: "claude-opus-4-6", name: "Opus", provider: "anthropic" },
|
||||
{ id: "gpt-4.1-mini", name: "GPT-4.1 Mini", provider: "openai" },
|
||||
];
|
||||
vi.mocked(loadModelCatalog).mockResolvedValueOnce(catalog);
|
||||
vi.mocked(loadManifestModelCatalog).mockReturnValueOnce(catalog);
|
||||
]);
|
||||
|
||||
await runAgentWithSessionKey("agent:main:subagent:clear-overrides");
|
||||
|
||||
@@ -877,16 +877,14 @@ describe("agentCommand", () => {
|
||||
"openai/gpt-4.1-mini": {},
|
||||
},
|
||||
});
|
||||
const catalog = [
|
||||
mockModelCatalogOnce([
|
||||
{
|
||||
id: "gpt-4.1-mini",
|
||||
name: "GPT-4.1 Mini",
|
||||
provider: "openai",
|
||||
reasoning: true,
|
||||
},
|
||||
];
|
||||
vi.mocked(loadModelCatalog).mockResolvedValueOnce(catalog);
|
||||
vi.mocked(loadManifestModelCatalog).mockReturnValueOnce(catalog);
|
||||
]);
|
||||
|
||||
await agentCommand({ message: "hi", to: "+1555" }, runtime);
|
||||
|
||||
|
||||
@@ -90,15 +90,16 @@ function createPluginCandidate(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function createRichPluginFixture(params: { packageVersion?: string } = {}) {
|
||||
function createRichPluginFixture(params: { id?: string; packageVersion?: string } = {}) {
|
||||
const rootDir = makeTempDir();
|
||||
const id = params.id ?? "demo";
|
||||
writeRuntimeEntry(rootDir);
|
||||
writePackageJson(rootDir, {
|
||||
name: "@vendor/demo-plugin",
|
||||
name: `@vendor/${id}`,
|
||||
version: params.packageVersion ?? "1.2.3",
|
||||
});
|
||||
writePluginManifest(rootDir, {
|
||||
id: "demo",
|
||||
id,
|
||||
name: "Demo",
|
||||
configSchema: { type: "object" },
|
||||
providers: ["demo"],
|
||||
@@ -566,6 +567,39 @@ describe("installed plugin index", () => {
|
||||
expect(isInstalledPluginEnabled(index, "demo", config)).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps an index-disabled plugin disabled when config only enables another plugin", () => {
|
||||
const enabledFixture = createRichPluginFixture({ id: "enabled-demo" });
|
||||
const disabledFixture = createRichPluginFixture({ id: "disabled-demo" });
|
||||
const index = loadInstalledPluginIndex({
|
||||
candidates: [enabledFixture.candidate, disabledFixture.candidate],
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
"disabled-demo": {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
env: hermeticEnv(),
|
||||
});
|
||||
|
||||
expect(index.plugins.find((plugin) => plugin.pluginId === "disabled-demo")?.enabled).toBe(
|
||||
false,
|
||||
);
|
||||
expect(
|
||||
isInstalledPluginEnabled(index, "disabled-demo", {
|
||||
plugins: {
|
||||
entries: {
|
||||
"enabled-demo": {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("uses runtime plugin id normalization for legacy enablement aliases", () => {
|
||||
const rootDir = makeTempDir();
|
||||
writeRuntimeEntry(rootDir);
|
||||
|
||||
@@ -136,11 +136,12 @@ export function isInstalledPluginEnabled(
|
||||
return record.enabled;
|
||||
}
|
||||
const normalizedConfig = normalizePluginsConfig(config?.plugins);
|
||||
return resolveEffectiveEnableState({
|
||||
const state = resolveEffectiveEnableState({
|
||||
id: record.pluginId,
|
||||
origin: record.origin,
|
||||
config: normalizedConfig,
|
||||
rootConfig: config,
|
||||
enabledByDefault: record.enabledByDefault,
|
||||
}).enabled;
|
||||
});
|
||||
return state.enabled && (record.enabled || state.explicitlyEnabled === true);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user