fix: stabilize release validation

This commit is contained in:
Peter Steinberger
2026-05-02 07:10:33 +01:00
parent 71da5af164
commit bf6a02c6da
7 changed files with 107 additions and 25 deletions

View File

@@ -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",

View File

@@ -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")

View File

@@ -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]) {

View File

@@ -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",

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);
}