refactor(channels): make built-in channels self-contained (#4908)
* refactor(channels): own setup and instance contracts * refactor(channels): isolate management contracts * refactor(channels): normalize activation contracts * fix(channels): enforce management contracts * refactor(channels): finish setup ownership migration * fix(channels): harden management contracts * fix(channels): enforce lazy loading and runtime ownership * fix(feishu): make multi-instance startup idempotent * fix(webui): render channel setup contracts cleanly * fix(feishu): stop websocket clients cleanly * fix(channels): enforce persistence and activation gates * fix(channels): preserve global feature action scope * fix(channels): apply defaults for single plugins * fix(channels): enforce management contract boundaries * refactor(feishu): remove identity helper indirection * fix(channels): preserve management setup contracts * refactor(channels): generalize instance settings UI * refactor(channels): package channel plugins with web UI metadata * refactor(channels): make built-ins self-contained packages * test(channels): colocate tests with channel packages * fix(dingtalk): use official brand icon * feat(channels): colocate webui translations * docs(channels): clarify plugin ownership * test(exec): remove output wait race * refactor(channels): unify plugin descriptors * fix(channels): enforce descriptor-owned contracts * refactor(channels): finish package-owned plugin setup * refactor(channels): use repository-owned packages only * fix(channels): self-describe dependencies and runtime state * fix(channels): warn about legacy entry points
This commit is contained in:
@@ -0,0 +1,146 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
channelIsRunning,
|
||||
channelSetup,
|
||||
channelStatusLabel,
|
||||
channelToggleChecked,
|
||||
} from "@/components/settings/channels/ChannelIdentity";
|
||||
import type { NanobotFeatureInfo } from "@/lib/types";
|
||||
|
||||
function feature(overrides: Partial<NanobotFeatureInfo>): NanobotFeatureInfo {
|
||||
return {
|
||||
name: "plugin-chat",
|
||||
display_name: "Plugin Chat",
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
installed: true,
|
||||
ready: false,
|
||||
status: "not_enabled",
|
||||
install_supported: true,
|
||||
requires_restart: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("channelSetup", () => {
|
||||
it("builds editable fields for a plugin-owned backend contract", () => {
|
||||
const setup = channelSetup(feature({
|
||||
setup: {
|
||||
fields: [
|
||||
{
|
||||
key: "channels.plugin-chat.apiToken",
|
||||
field: "apiToken",
|
||||
kind: "secret",
|
||||
choices: [],
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
key: "channels.plugin-chat.region",
|
||||
field: "region",
|
||||
kind: "enum",
|
||||
choices: ["us", "eu"],
|
||||
required: false,
|
||||
},
|
||||
],
|
||||
official_url: "https://plugin.example/setup",
|
||||
},
|
||||
}));
|
||||
|
||||
expect(setup.officialUrl).toBe("https://plugin.example/setup");
|
||||
expect(setup.officialLabel).toBe("Open official setup");
|
||||
expect(setup.fields).toEqual([
|
||||
expect.objectContaining({
|
||||
key: "channels.plugin-chat.apiToken",
|
||||
label: "Api Token",
|
||||
secret: true,
|
||||
optional: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
key: "channels.plugin-chat.region",
|
||||
options: [
|
||||
{ value: "us", label: "Us" },
|
||||
{ value: "eu", label: "Eu" },
|
||||
],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("filters catalog-only fields that the backend does not accept", () => {
|
||||
const setup = channelSetup(feature({
|
||||
name: "discord",
|
||||
display_name: "Discord",
|
||||
webui: "webui/index.ts",
|
||||
setup: {
|
||||
fields: [{
|
||||
key: "channels.discord.token",
|
||||
field: "token",
|
||||
kind: "secret",
|
||||
choices: [],
|
||||
required: true,
|
||||
}],
|
||||
},
|
||||
}));
|
||||
|
||||
expect(setup.fields?.map((field) => field.key)).toEqual(["channels.discord.token"]);
|
||||
expect(setup.manualFields).toBeUndefined();
|
||||
});
|
||||
|
||||
it("uses backend defaults and choices with catalog presentation labels", () => {
|
||||
const setup = channelSetup(feature({
|
||||
name: "discord",
|
||||
display_name: "Discord",
|
||||
webui: "webui/index.ts",
|
||||
setup: {
|
||||
fields: [{
|
||||
key: "channels.discord.groupPolicy",
|
||||
field: "groupPolicy",
|
||||
kind: "enum",
|
||||
choices: ["open"],
|
||||
required: false,
|
||||
default_value: "open",
|
||||
}],
|
||||
},
|
||||
}));
|
||||
|
||||
expect(setup.fields).toEqual([
|
||||
expect.objectContaining({
|
||||
key: "channels.discord.groupPolicy",
|
||||
label: "Group behavior",
|
||||
defaultValue: "open",
|
||||
options: [{ value: "open", label: "All messages" }],
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("loads setup copy from the channel-owned locale", () => {
|
||||
const setup = channelSetup(feature({
|
||||
name: "dingtalk",
|
||||
display_name: "DingTalk",
|
||||
webui: "webui/index.ts",
|
||||
}), "zh-CN");
|
||||
|
||||
expect(setup.summary).toBe("钉钉需要 Stream 模式的应用凭据。");
|
||||
expect(setup.steps[0]).toBe("创建或选择一个已启用 Stream 模式的钉钉应用。");
|
||||
expect(setup.fields).toContainEqual(expect.objectContaining({
|
||||
key: "channels.dingtalk.allowFrom",
|
||||
label: "允许的用户",
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("channel runtime state", () => {
|
||||
const tx = (_key: string, fallback: string) => fallback;
|
||||
|
||||
it("only reports a channel on when the runtime is explicitly running", () => {
|
||||
const running = feature({ enabled: true, runtime_status: "running" });
|
||||
const unknown = feature({ enabled: true });
|
||||
|
||||
expect(channelIsRunning(running)).toBe(true);
|
||||
expect(channelToggleChecked(running)).toBe(true);
|
||||
expect(channelStatusLabel(running, tx)).toBe("On");
|
||||
expect(channelIsRunning(unknown)).toBe(false);
|
||||
expect(channelToggleChecked(unknown)).toBe(false);
|
||||
expect(channelStatusLabel(unknown, tx)).toBe("Not running");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user