nanobot/webui/src/tests/channel-identity.test.ts
chengyongru 462a0dfb0f
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
2026-07-19 23:30:49 +08:00

147 lines
4.2 KiB
TypeScript

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