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:
@@ -3,7 +3,11 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { SettingsView } from "@/components/settings/SettingsView";
|
||||
import { ClientProvider } from "@/providers/ClientProvider";
|
||||
import type { SettingsPayload } from "@/lib/types";
|
||||
import type {
|
||||
ChannelSetupContract,
|
||||
ChannelSetupContractField,
|
||||
SettingsPayload,
|
||||
} from "@/lib/types";
|
||||
|
||||
function jsonResponse(body: unknown): Response {
|
||||
return {
|
||||
@@ -120,6 +124,118 @@ function settingsPayload(): SettingsPayload {
|
||||
};
|
||||
}
|
||||
|
||||
function channelSetupField(
|
||||
channel: string,
|
||||
field: string,
|
||||
kind: ChannelSetupContractField["kind"] = "string",
|
||||
options: {
|
||||
required?: boolean;
|
||||
choices?: string[];
|
||||
defaultValue?: string;
|
||||
} = {},
|
||||
): ChannelSetupContractField {
|
||||
return {
|
||||
key: `channels.${channel}.${field}`,
|
||||
field,
|
||||
kind,
|
||||
choices: options.choices ?? [],
|
||||
required: options.required ?? false,
|
||||
...(options.defaultValue === undefined ? {} : { default_value: options.defaultValue }),
|
||||
};
|
||||
}
|
||||
|
||||
function channelSetupContract(
|
||||
channel: "discord" | "email" | "feishu" | "matrix" | "qq",
|
||||
): ChannelSetupContract {
|
||||
const field = (
|
||||
name: string,
|
||||
kind: ChannelSetupContractField["kind"] = "string",
|
||||
options: Parameters<typeof channelSetupField>[3] = {},
|
||||
) => channelSetupField(channel, name, kind, options);
|
||||
|
||||
switch (channel) {
|
||||
case "discord":
|
||||
return {
|
||||
official_url: "https://discord.com/developers/applications",
|
||||
fields: [
|
||||
field("token", "secret", { required: true }),
|
||||
field("allowFrom", "list"),
|
||||
field("allowChannels", "list"),
|
||||
field("groupPolicy", "enum", {
|
||||
choices: ["mention", "open"],
|
||||
defaultValue: "mention",
|
||||
}),
|
||||
],
|
||||
};
|
||||
case "email":
|
||||
return {
|
||||
official_url: "https://support.google.com/accounts/answer/185833",
|
||||
fields: [
|
||||
field("consentGranted", "bool", { required: true, defaultValue: "false" }),
|
||||
field("imapHost", "string", { required: true }),
|
||||
field("imapPort", "int"),
|
||||
field("imapUsername", "string", { required: true }),
|
||||
field("imapPassword", "secret", { required: true }),
|
||||
field("smtpHost", "string", { required: true }),
|
||||
field("smtpPort", "int"),
|
||||
field("smtpUsername", "string", { required: true }),
|
||||
field("smtpPassword", "secret", { required: true }),
|
||||
field("fromAddress"),
|
||||
field("pollIntervalSeconds", "int"),
|
||||
field("allowFrom", "list"),
|
||||
field("verifyDkim", "bool", { defaultValue: "true" }),
|
||||
field("verifySpf", "bool", { defaultValue: "true" }),
|
||||
],
|
||||
};
|
||||
case "feishu":
|
||||
return {
|
||||
official_url: "https://open.feishu.cn/app",
|
||||
fields: [
|
||||
field("appId", "string", { required: true }),
|
||||
field("appSecret", "secret", { required: true }),
|
||||
field("domain", "enum", {
|
||||
choices: ["feishu", "lark"],
|
||||
defaultValue: "feishu",
|
||||
}),
|
||||
field("groupPolicy", "enum", {
|
||||
choices: ["mention", "open"],
|
||||
defaultValue: "mention",
|
||||
}),
|
||||
field("allowFrom", "list"),
|
||||
field("topicIsolation", "bool"),
|
||||
],
|
||||
};
|
||||
case "matrix":
|
||||
return {
|
||||
official_url: "https://matrix.org/ecosystem/clients/",
|
||||
fields: [
|
||||
field("homeserver", "string", { required: true }),
|
||||
field("userId", "string", { required: true }),
|
||||
field("password", "secret"),
|
||||
field("accessToken", "secret"),
|
||||
field("deviceId"),
|
||||
field("groupPolicy", "enum", {
|
||||
choices: ["allowlist", "mention", "open"],
|
||||
defaultValue: "open",
|
||||
}),
|
||||
],
|
||||
};
|
||||
case "qq":
|
||||
return {
|
||||
official_url: "https://q.qq.com/",
|
||||
fields: [
|
||||
field("appId", "string", { required: true }),
|
||||
field("secret", "secret", { required: true }),
|
||||
field("allowFrom", "list"),
|
||||
field("msgFormat", "enum", {
|
||||
choices: ["markdown", "plain"],
|
||||
defaultValue: "plain",
|
||||
}),
|
||||
],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function autoDynamicProviderPayload(
|
||||
options: {
|
||||
configured: boolean;
|
||||
@@ -417,6 +533,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "matrix",
|
||||
display_name: "Matrix",
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
installed: false,
|
||||
@@ -433,8 +550,11 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "matrix",
|
||||
display_name: "Matrix",
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
running: true,
|
||||
runtime_status: "running",
|
||||
installed: true,
|
||||
ready: true,
|
||||
status: "enabled",
|
||||
@@ -450,6 +570,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "matrix",
|
||||
display_name: "Matrix",
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
installed: true,
|
||||
@@ -519,7 +640,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
expect(screen.queryByText("Disabled channel 'matrix'")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows enabled nanobot channels with missing support as enabled", async () => {
|
||||
it("shows an enabled channel with missing support as failed", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/settings") return jsonResponse(settingsPayload());
|
||||
@@ -532,6 +653,9 @@ describe("SettingsView Apps catalog", () => {
|
||||
display_name: "Matrix",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
running: false,
|
||||
runtime_status: "failed",
|
||||
runtime_error: "Channel dependencies could not be installed. Check gateway logs.",
|
||||
installed: false,
|
||||
ready: false,
|
||||
status: "missing_dependency",
|
||||
@@ -565,11 +689,11 @@ describe("SettingsView Apps catalog", () => {
|
||||
renderSettingsView({ initialSection: "channels" });
|
||||
|
||||
expect(await screen.findByRole("button", { name: "View Matrix settings" })).toBeInTheDocument();
|
||||
expect(screen.getByText("1 enabled · 1 channels")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("On").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("0 running · 1 channels")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Failed").length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText("Enabled, support needs install")).not.toBeInTheDocument();
|
||||
|
||||
expect(screen.getByRole("switch", { name: "Matrix channel" })).toHaveAttribute("aria-checked", "true");
|
||||
expect(screen.getByRole("switch", { name: "Matrix channel" })).toHaveAttribute("aria-checked", "false");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Install support" }));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Install and enable" }));
|
||||
|
||||
@@ -583,6 +707,51 @@ describe("SettingsView Apps catalog", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("shows a configured channel as failed when its runtime did not start", async () => {
|
||||
const runtimeError = "Channel failed to start. Check gateway logs.";
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/settings") return jsonResponse(settingsPayload());
|
||||
if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
|
||||
if (url === "/api/settings/mcp-presets") return jsonResponse({ presets: [], installed_count: 0 });
|
||||
if (url === "/api/settings/nanobot-features") {
|
||||
return jsonResponse({
|
||||
features: [{
|
||||
name: "matrix",
|
||||
display_name: "Matrix",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
installed: true,
|
||||
ready: false,
|
||||
running: false,
|
||||
runtime_status: "failed",
|
||||
runtime_error: runtimeError,
|
||||
status: "failed",
|
||||
install_supported: true,
|
||||
requires_restart: false,
|
||||
}],
|
||||
enabled_count: 0,
|
||||
});
|
||||
}
|
||||
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
||||
}),
|
||||
);
|
||||
|
||||
renderSettingsView({ initialSection: "channels" });
|
||||
|
||||
expect(await screen.findByRole("button", { name: "View Matrix settings" })).toBeInTheDocument();
|
||||
expect(screen.getByText("0 running · 1 channels")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Failed").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText(runtimeError)).toBeInTheDocument();
|
||||
expect(screen.getByRole("switch", { name: "Matrix channel" })).toHaveAttribute(
|
||||
"aria-checked",
|
||||
"false",
|
||||
);
|
||||
});
|
||||
|
||||
it("starts Feishu connect in WebUI instead of showing a CLI command", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
@@ -594,6 +763,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "feishu",
|
||||
display_name: "Feishu",
|
||||
webui: "webui/index.tsx",
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
configured: false,
|
||||
@@ -651,6 +821,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "feishu",
|
||||
display_name: "Feishu",
|
||||
webui: "webui/index.tsx",
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
configured: false,
|
||||
@@ -706,6 +877,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "feishu",
|
||||
display_name: "Feishu",
|
||||
webui: "webui/index.tsx",
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
configured: true,
|
||||
@@ -723,16 +895,24 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "feishu",
|
||||
display_name: "Feishu",
|
||||
webui: "webui/index.tsx",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
running: true,
|
||||
runtime_status: "running",
|
||||
configured: true,
|
||||
instances: [{
|
||||
id: "default",
|
||||
name: "nanobot",
|
||||
domain: "feishu",
|
||||
enabled: true,
|
||||
running: true,
|
||||
runtime_status: "running",
|
||||
configured: true,
|
||||
app_id: "cli_test",
|
||||
config_values: { "channels.feishu.appId": "cli_test" },
|
||||
configured_fields: [
|
||||
"channels.feishu.appId",
|
||||
"channels.feishu.appSecret",
|
||||
],
|
||||
}],
|
||||
installed: true,
|
||||
ready: true,
|
||||
@@ -787,6 +967,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "feishu",
|
||||
display_name: "Feishu",
|
||||
webui: "webui/index.tsx",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
@@ -795,26 +976,42 @@ describe("SettingsView Apps catalog", () => {
|
||||
status: "enabled",
|
||||
install_supported: true,
|
||||
requires_restart: true,
|
||||
setup: channelSetupContract("feishu"),
|
||||
instances: [
|
||||
{
|
||||
id: "default",
|
||||
name: "nanobot",
|
||||
display_name: "Support Bot",
|
||||
avatar_url: "https://example.com/support.png",
|
||||
domain: "feishu",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
app_id: "cli_default",
|
||||
config_values: {
|
||||
"channels.feishu.appId": "cli_default",
|
||||
"channels.feishu.domain": "feishu",
|
||||
"channels.feishu.groupPolicy": "mention",
|
||||
"channels.feishu.allowFrom": "",
|
||||
"channels.feishu.topicIsolation": "true",
|
||||
},
|
||||
configured_fields: [
|
||||
"channels.feishu.appId",
|
||||
"channels.feishu.appSecret",
|
||||
"channels.feishu.domain",
|
||||
"channels.feishu.groupPolicy",
|
||||
"channels.feishu.topicIsolation",
|
||||
],
|
||||
},
|
||||
{
|
||||
id: "product",
|
||||
name: "Product bot",
|
||||
display_name: "Product Helper",
|
||||
avatar_url: "https://example.com/product.png",
|
||||
domain: "feishu",
|
||||
enabled: false,
|
||||
configured: true,
|
||||
app_id: "cli_product",
|
||||
config_values: { "channels.feishu.appId": "cli_product" },
|
||||
configured_fields: [
|
||||
"channels.feishu.appId",
|
||||
"channels.feishu.appSecret",
|
||||
],
|
||||
},
|
||||
],
|
||||
}],
|
||||
@@ -847,6 +1044,8 @@ describe("SettingsView Apps catalog", () => {
|
||||
"true",
|
||||
);
|
||||
expect(screen.getAllByText("cli_def...ault").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText("Advanced")).toBeInTheDocument();
|
||||
expect(screen.getByText("Topic isolation")).toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /Product Helper/ }));
|
||||
expect(screen.getByRole("button", { name: /Support Bot/ })).toHaveAttribute(
|
||||
@@ -859,18 +1058,98 @@ describe("SettingsView Apps catalog", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("renders external multi-instance channels from the shared contract", async () => {
|
||||
const fetchMock = vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/settings") return jsonResponse(settingsPayload());
|
||||
if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
|
||||
if (url === "/api/settings/mcp-presets") return jsonResponse({ presets: [], installed_count: 0 });
|
||||
if (url === "/api/settings/nanobot-features") {
|
||||
return jsonResponse({
|
||||
features: [{
|
||||
name: "multiplugin",
|
||||
display_name: "Multi Plugin",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
running: true,
|
||||
runtime_status: "running",
|
||||
configured: true,
|
||||
installed: true,
|
||||
ready: true,
|
||||
status: "enabled",
|
||||
install_supported: true,
|
||||
requires_restart: false,
|
||||
setup: {
|
||||
fields: [
|
||||
channelSetupField("multiplugin", "token", "secret", { required: true }),
|
||||
channelSetupField("multiplugin", "region", "enum", {
|
||||
choices: ["eu", "us"],
|
||||
defaultValue: "us",
|
||||
}),
|
||||
],
|
||||
},
|
||||
instances: [
|
||||
{
|
||||
id: "default",
|
||||
name: "Default worker",
|
||||
enabled: true,
|
||||
running: true,
|
||||
runtime_status: "running",
|
||||
configured: true,
|
||||
config_values: { "channels.multiplugin.region": "us" },
|
||||
configured_fields: ["channels.multiplugin.token"],
|
||||
},
|
||||
{
|
||||
id: "product",
|
||||
name: "Product worker",
|
||||
enabled: true,
|
||||
running: true,
|
||||
runtime_status: "running",
|
||||
configured: true,
|
||||
config_values: { "channels.multiplugin.region": "eu" },
|
||||
configured_fields: ["channels.multiplugin.token"],
|
||||
},
|
||||
],
|
||||
}],
|
||||
enabled_count: 1,
|
||||
});
|
||||
}
|
||||
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
||||
});
|
||||
vi.stubGlobal("fetch", fetchMock);
|
||||
|
||||
renderSettingsView({ initialSection: "channels" });
|
||||
|
||||
expect(await screen.findByText("Default worker")).toBeInTheDocument();
|
||||
expect(screen.queryByRole("switch", { name: "Multi Plugin channel" })).not.toBeInTheDocument();
|
||||
expect(screen.getByRole("switch", { name: "Default worker instance" })).toHaveAttribute(
|
||||
"aria-checked",
|
||||
"true",
|
||||
);
|
||||
expect(screen.getByRole("switch", { name: "Product worker instance" })).toHaveAttribute(
|
||||
"aria-checked",
|
||||
"true",
|
||||
);
|
||||
fireEvent.click(screen.getByRole("button", { name: "Product worker" }));
|
||||
expect(screen.getByRole("radio", { name: "Eu" })).toHaveAttribute("aria-checked", "true");
|
||||
expect(screen.getByText("Saved")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows a single Feishu assistant without a duplicate assistant list", async () => {
|
||||
const reconnectUrls: string[] = [];
|
||||
const feishuPayload = {
|
||||
features: [{
|
||||
name: "feishu",
|
||||
display_name: "Feishu",
|
||||
webui: "webui/index.tsx",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
installed: true,
|
||||
ready: true,
|
||||
status: "enabled",
|
||||
running: true,
|
||||
runtime_status: "running",
|
||||
install_supported: true,
|
||||
requires_restart: true,
|
||||
instances: [{
|
||||
@@ -878,10 +1157,15 @@ describe("SettingsView Apps catalog", () => {
|
||||
name: "nanobot",
|
||||
display_name: "Support Bot",
|
||||
avatar_url: "https://example.com/support.png",
|
||||
domain: "feishu",
|
||||
enabled: true,
|
||||
running: true,
|
||||
runtime_status: "running",
|
||||
configured: true,
|
||||
app_id: "cli_support",
|
||||
config_values: { "channels.feishu.appId": "cli_support" },
|
||||
configured_fields: [
|
||||
"channels.feishu.appId",
|
||||
"channels.feishu.appSecret",
|
||||
],
|
||||
}],
|
||||
}],
|
||||
enabled_count: 1,
|
||||
@@ -920,6 +1204,67 @@ describe("SettingsView Apps catalog", () => {
|
||||
expect(document.querySelector('img[src="https://example.com/support.png"]')).toBeTruthy();
|
||||
});
|
||||
|
||||
it("does not call a configured Feishu assistant connected after runtime failure", async () => {
|
||||
const runtimeError = "Channel failed to start. Check gateway logs.";
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url === "/api/settings") return jsonResponse(settingsPayload());
|
||||
if (url === "/api/settings/cli-apps") return jsonResponse({ apps: [], installed_count: 0 });
|
||||
if (url === "/api/settings/mcp-presets") return jsonResponse({ presets: [], installed_count: 0 });
|
||||
if (url === "/api/settings/nanobot-features") {
|
||||
return jsonResponse({
|
||||
features: [{
|
||||
name: "feishu",
|
||||
display_name: "Feishu",
|
||||
webui: "webui/index.tsx",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
installed: true,
|
||||
ready: false,
|
||||
running: false,
|
||||
runtime_status: "failed",
|
||||
runtime_error: runtimeError,
|
||||
status: "failed",
|
||||
install_supported: true,
|
||||
requires_restart: false,
|
||||
instances: [{
|
||||
id: "default",
|
||||
name: "test",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
running: false,
|
||||
runtime_status: "failed",
|
||||
runtime_error: runtimeError,
|
||||
config_values: { "channels.feishu.appId": "cli_test" },
|
||||
configured_fields: [
|
||||
"channels.feishu.appId",
|
||||
"channels.feishu.appSecret",
|
||||
],
|
||||
}],
|
||||
}],
|
||||
enabled_count: 0,
|
||||
});
|
||||
}
|
||||
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
||||
}),
|
||||
);
|
||||
|
||||
renderSettingsView({ initialSection: "channels" });
|
||||
|
||||
await screen.findByText("No assistant connected");
|
||||
expect(screen.getByText("0 running · 1 channels")).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Failed").length).toBeGreaterThan(0);
|
||||
expect(screen.getByText(runtimeError)).toBeInTheDocument();
|
||||
expect(screen.getByRole("switch", { name: "test assistant" })).toHaveAttribute(
|
||||
"aria-checked",
|
||||
"false",
|
||||
);
|
||||
expect(screen.queryByText("Connected")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows group behavior fields as options", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
@@ -933,6 +1278,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "discord",
|
||||
display_name: "Discord",
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
installed: true,
|
||||
@@ -940,6 +1286,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
status: "enabled",
|
||||
install_supported: true,
|
||||
requires_restart: true,
|
||||
setup: channelSetupContract("discord"),
|
||||
}],
|
||||
enabled_count: 1,
|
||||
});
|
||||
@@ -1039,6 +1386,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "discord",
|
||||
display_name: "Discord",
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
configured: false,
|
||||
@@ -1047,6 +1395,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
status: "not_enabled",
|
||||
install_supported: true,
|
||||
requires_restart: true,
|
||||
setup: channelSetupContract("discord"),
|
||||
}],
|
||||
enabled_count: 0,
|
||||
});
|
||||
@@ -1064,14 +1413,18 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "discord",
|
||||
display_name: "Discord",
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
running: true,
|
||||
runtime_status: "running",
|
||||
configured: true,
|
||||
installed: true,
|
||||
ready: true,
|
||||
status: "enabled",
|
||||
install_supported: true,
|
||||
requires_restart: true,
|
||||
setup: channelSetupContract("discord"),
|
||||
}],
|
||||
enabled_count: 1,
|
||||
requires_restart: false,
|
||||
@@ -1150,6 +1503,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "discord",
|
||||
display_name: "Discord",
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
configured: true,
|
||||
@@ -1167,6 +1521,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
"channels.discord.allowChannels",
|
||||
"channels.discord.groupPolicy",
|
||||
],
|
||||
setup: channelSetupContract("discord"),
|
||||
}],
|
||||
enabled_count: 0,
|
||||
});
|
||||
@@ -1176,6 +1531,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "discord",
|
||||
display_name: "Discord",
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
configured: true,
|
||||
@@ -1184,6 +1540,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
status: "enabled",
|
||||
install_supported: true,
|
||||
requires_restart: true,
|
||||
setup: channelSetupContract("discord"),
|
||||
}],
|
||||
enabled_count: 1,
|
||||
requires_restart: false,
|
||||
@@ -1203,7 +1560,10 @@ describe("SettingsView Apps catalog", () => {
|
||||
expect(screen.getByRole("switch", { name: "Discord channel" })).toBeEnabled();
|
||||
expect(screen.getByText("Configured manually")).toBeInTheDocument();
|
||||
expect(screen.getByText("Saved")).toBeInTheDocument();
|
||||
expect(screen.getByPlaceholderText("Saved secret")).toHaveValue("");
|
||||
const savedSecret = screen.getByPlaceholderText("Saved secret");
|
||||
expect(savedSecret).toHaveValue("");
|
||||
expect(savedSecret).toHaveAttribute("autocomplete", "off");
|
||||
expect(savedSecret.closest("form")).not.toBeNull();
|
||||
expect(screen.queryByDisplayValue("discord-secret-token")).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByText("Advanced"));
|
||||
@@ -1235,6 +1595,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "telegram",
|
||||
display_name: "Telegram",
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
installed: true,
|
||||
@@ -1291,6 +1652,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: channels.map(([name, displayName]) => ({
|
||||
name,
|
||||
display_name: displayName,
|
||||
webui: ["feishu", "weixin"].includes(name) ? "webui/index.tsx" : "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: name === "websocket",
|
||||
installed: true,
|
||||
@@ -1301,6 +1663,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
})).concat(hiddenChannels.map(([name, displayName]) => ({
|
||||
name,
|
||||
display_name: displayName,
|
||||
settings_visible: false,
|
||||
type: "channel",
|
||||
enabled: false,
|
||||
installed: true,
|
||||
@@ -1343,6 +1706,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: ["email", "feishu", "matrix", "qq"].map((name) => ({
|
||||
name,
|
||||
display_name: name === "qq" ? "QQ" : name[0].toUpperCase() + name.slice(1),
|
||||
webui: name === "feishu" ? "webui/index.tsx" : "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
installed: true,
|
||||
@@ -1350,6 +1714,7 @@ describe("SettingsView Apps catalog", () => {
|
||||
status: "enabled",
|
||||
install_supported: true,
|
||||
requires_restart: true,
|
||||
setup: channelSetupContract(name as "email" | "feishu" | "matrix" | "qq"),
|
||||
})),
|
||||
enabled_count: 4,
|
||||
});
|
||||
@@ -1409,6 +1774,8 @@ describe("SettingsView Apps catalog", () => {
|
||||
features: [{
|
||||
name: "websocket",
|
||||
display_name: "Websocket",
|
||||
capabilities: ["always_enabled"],
|
||||
webui: "webui/index.ts",
|
||||
type: "channel",
|
||||
enabled: true,
|
||||
installed: true,
|
||||
|
||||
Reference in New Issue
Block a user