mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
fix(config): render Feishu group policy in Form mode (#107336)
* fix(ui): scope unsupported config guidance to fields * fix(config): export Feishu transform inputs * chore: remove release-owned changelog entry
This commit is contained in:
committed by
GitHub
parent
b68714ee42
commit
3229fbdc48
@@ -61,7 +61,6 @@ import type {
|
||||
ClawdbotConfig,
|
||||
} from "./channel-runtime-api.js";
|
||||
import {
|
||||
buildChannelConfigSchema,
|
||||
buildProbeChannelStatusSummary,
|
||||
chunkTextForOutbound,
|
||||
createActionGate,
|
||||
@@ -71,7 +70,7 @@ import {
|
||||
} from "./channel-runtime-api.js";
|
||||
import { normalizeFeishuChatType, resolveFeishuChatType } from "./chat-type.js";
|
||||
import { isRecord } from "./comment-shared.js";
|
||||
import { FeishuConfigSchema } from "./config-schema.js";
|
||||
import { FeishuChannelConfigSchema } from "./config-schema.js";
|
||||
import {
|
||||
buildFeishuConversationId,
|
||||
buildFeishuModelOverrideParentCandidates,
|
||||
@@ -981,7 +980,7 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
|
||||
},
|
||||
reload: { configPrefixes: ["channels.feishu"] },
|
||||
doctor: feishuDoctor,
|
||||
configSchema: buildChannelConfigSchema(FeishuConfigSchema),
|
||||
configSchema: FeishuChannelConfigSchema,
|
||||
config: {
|
||||
...feishuConfigAdapter,
|
||||
setAccountEnabled: ({ cfg, accountId, enabled }) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Feishu tests cover config schema plugin behavior.
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { FeishuConfigSchema } from "./config-schema.js";
|
||||
import { FeishuChannelConfigSchema, FeishuConfigSchema } from "./config-schema.js";
|
||||
|
||||
// The NEGATIVE webhook fixtures below spread these bases and add
|
||||
// verificationToken separately so the GHSA-G353-MGV3-8PCJ opengrep pattern —
|
||||
@@ -62,6 +62,26 @@ describe("FeishuConfigSchema webhook validation", () => {
|
||||
expect(result.groupPolicy).toBe("open");
|
||||
});
|
||||
|
||||
it("exports legacy groupPolicy as a typed config input", () => {
|
||||
const expected = {
|
||||
anyOf: [
|
||||
{ type: "string", enum: ["open", "allowlist", "disabled"] },
|
||||
{ type: "string", const: "allowall" },
|
||||
],
|
||||
};
|
||||
|
||||
expect(FeishuChannelConfigSchema.schema).toMatchObject({
|
||||
properties: {
|
||||
groupPolicy: expected,
|
||||
accounts: {
|
||||
additionalProperties: {
|
||||
properties: { groupPolicy: expected },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects top-level webhook mode without verificationToken", () => {
|
||||
const result = FeishuConfigSchema.safeParse({
|
||||
connectionMode: "webhook",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
// Feishu helper module supports config schema behavior.
|
||||
import { normalizeAccountId } from "openclaw/plugin-sdk/account-id";
|
||||
import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
|
||||
import { z } from "zod";
|
||||
export { z };
|
||||
import { buildSecretInputSchema, hasConfiguredSecretInput } from "./secret-input.js";
|
||||
@@ -351,3 +352,7 @@ export const FeishuConfigSchema = z
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const FeishuChannelConfigSchema = buildChannelConfigSchema(FeishuConfigSchema, {
|
||||
jsonSchemaMode: "input",
|
||||
});
|
||||
|
||||
@@ -50,6 +50,33 @@ describe("buildChannelConfigSchema", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("can describe accepted transform inputs instead of unrepresentable outputs", () => {
|
||||
const result = buildChannelConfigSchema(
|
||||
z.object({
|
||||
policy: z.union([
|
||||
z.enum(["open", "disabled"]),
|
||||
z.literal("legacy").transform(() => "open" as const),
|
||||
]),
|
||||
}),
|
||||
{ jsonSchemaMode: "input" },
|
||||
);
|
||||
|
||||
expect(result.schema).toMatchObject({
|
||||
properties: {
|
||||
policy: {
|
||||
anyOf: [
|
||||
{ type: "string", enum: ["open", "disabled"] },
|
||||
{ type: "string", const: "legacy" },
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(result.runtime?.safeParse({ policy: "legacy" })).toEqual({
|
||||
success: true,
|
||||
data: { policy: "open" },
|
||||
});
|
||||
});
|
||||
|
||||
it("passes through ui hints and exposes a runtime parser", () => {
|
||||
const result = buildChannelConfigSchema(z.object({ enabled: z.boolean().default(true) }), {
|
||||
uiHints: { enabled: { label: "Enabled" } },
|
||||
|
||||
@@ -50,6 +50,8 @@ export function buildCatchallMultiAccountChannelSchema<T extends ExtendableZodOb
|
||||
|
||||
type BuildChannelConfigSchemaOptions = {
|
||||
uiHints?: Record<string, ChannelConfigUiHint>;
|
||||
/** Select input mode when transforms must expose accepted config values to editors. */
|
||||
jsonSchemaMode?: "input" | "output";
|
||||
};
|
||||
|
||||
type BuildJsonChannelConfigSchemaOptions = {
|
||||
@@ -146,6 +148,7 @@ export function buildChannelConfigSchema(
|
||||
return {
|
||||
schema: schemaWithJson.toJSONSchema({
|
||||
target: "draft-07",
|
||||
...(options?.jsonSchemaMode ? { io: options.jsonSchemaMode } : {}),
|
||||
unrepresentable: "any",
|
||||
}) as JsonSchemaObject,
|
||||
...(options?.uiHints ? { uiHints: options.uiHints } : {}),
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,127 @@
|
||||
// Control UI tests cover form support for transform-backed config fields.
|
||||
import { mkdir } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { chromium, type Browser } from "playwright";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import {
|
||||
canRunPlaywrightChromium,
|
||||
installMockGateway,
|
||||
resolvePlaywrightChromiumExecutablePath,
|
||||
startControlUiE2eServer,
|
||||
type ControlUiE2eServer,
|
||||
} from "../test-helpers/control-ui-e2e.ts";
|
||||
|
||||
const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath());
|
||||
const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath);
|
||||
const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1";
|
||||
const describeControlUiE2e = chromiumAvailable || !allowMissingChromium ? describe : describe.skip;
|
||||
|
||||
const globalWarning =
|
||||
"Your config contains fields the form editor can't safely represent. Use Raw mode to edit those entries.";
|
||||
const captureUiProofEnabled = process.env.OPENCLAW_CAPTURE_UI_PROOF === "1";
|
||||
const uiProofArtifactDir = path.join(
|
||||
process.cwd(),
|
||||
".artifacts",
|
||||
"control-ui-e2e",
|
||||
"config-form-guidance",
|
||||
);
|
||||
|
||||
let browser: Browser;
|
||||
let server: ControlUiE2eServer;
|
||||
|
||||
describeControlUiE2e("Control UI config form guidance mocked Gateway E2E", () => {
|
||||
beforeAll(async () => {
|
||||
if (!chromiumAvailable) {
|
||||
throw new Error(
|
||||
`Playwright Chromium is not installed or cannot start at ${chromiumExecutablePath}. Run \`pnpm --dir ui exec playwright install --with-deps chromium\`, or set OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM=1 only when intentionally skipping this lane.`,
|
||||
);
|
||||
}
|
||||
server = await startControlUiE2eServer();
|
||||
browser = await chromium.launch({ executablePath: chromiumExecutablePath });
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close();
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
it("renders every accepted branch of a transform input schema", async () => {
|
||||
const context = await browser.newContext({
|
||||
colorScheme: "dark",
|
||||
locale: "en-US",
|
||||
serviceWorkers: "block",
|
||||
viewport: { height: 1000, width: 1440 },
|
||||
});
|
||||
const page = await context.newPage();
|
||||
const config = { update: { groupPolicy: "allowlist" } };
|
||||
await installMockGateway(page, {
|
||||
methodResponses: {
|
||||
"config.get": {
|
||||
config,
|
||||
hash: "config-form-guidance-e2e",
|
||||
issues: [],
|
||||
raw: JSON.stringify(config),
|
||||
valid: true,
|
||||
},
|
||||
"config.schema": {
|
||||
generatedAt: "2026-07-14T00:00:00.000Z",
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
update: {
|
||||
type: "object",
|
||||
title: "Updates",
|
||||
properties: {
|
||||
groupPolicy: {
|
||||
title: "Group policy",
|
||||
anyOf: [
|
||||
{ type: "string", enum: ["open", "allowlist", "disabled"] },
|
||||
{ type: "string", const: "allowall" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
uiHints: {},
|
||||
version: "e2e",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await page.goto(`${server.baseUrl}settings/general`);
|
||||
expect(response?.status()).toBe(200);
|
||||
|
||||
await page.locator('.content-header wa-radio[value="advanced"]').click();
|
||||
await page.getByRole("button", { name: "Core" }).click();
|
||||
await page.getByRole("button", { name: "Updates", exact: true }).click();
|
||||
|
||||
const policyRow = page.locator(".settings-row").filter({ hasText: "Group policy" });
|
||||
await expect.poll(() => policyRow.locator("wa-radio").count()).toBe(4);
|
||||
await expect.poll(() => policyRow.getByText("open", { exact: true }).count()).toBe(1);
|
||||
await expect.poll(() => policyRow.getByText("allowlist", { exact: true }).count()).toBe(1);
|
||||
await expect.poll(() => policyRow.getByText("disabled", { exact: true }).count()).toBe(1);
|
||||
await expect.poll(() => policyRow.getByText("allowall", { exact: true }).count()).toBe(1);
|
||||
await expect
|
||||
.poll(() => page.getByText("Unsupported schema node. Use Raw mode.").count())
|
||||
.toBe(0);
|
||||
await expect.poll(() => page.getByText(globalWarning).count()).toBe(0);
|
||||
|
||||
if (captureUiProofEnabled) {
|
||||
await mkdir(uiProofArtifactDir, { recursive: true });
|
||||
await page.screenshot({
|
||||
animations: "disabled",
|
||||
fullPage: true,
|
||||
path: path.join(uiProofArtifactDir, "01-transform-field-supported.png"),
|
||||
});
|
||||
}
|
||||
|
||||
await page.getByRole("button", { name: "Raw", exact: true }).click();
|
||||
await expect.poll(() => page.locator(".config-raw-field textarea").count()).toBe(1);
|
||||
await expect.poll(() => page.getByText(globalWarning).count()).toBe(0);
|
||||
} finally {
|
||||
await context.close();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user