mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-13 07:39:15 +03:00
1295 lines
49 KiB
TypeScript
1295 lines
49 KiB
TypeScript
import { fireEvent, screen, waitFor, within } from "@testing-library/react";
|
|
import { expect, it, vi } from "vitest";
|
|
import type { ChannelSetupContract, ChannelSetupContractField } from "@/lib/types";
|
|
import { requestMutationMock, jsonResponse, settingsPayload, renderSettingsView, installSettingsViewTestHooks } from "@/tests/settings-test-utils";
|
|
|
|
|
|
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",
|
|
}),
|
|
],
|
|
};
|
|
}
|
|
}
|
|
|
|
describe("Settings channels", () => {
|
|
installSettingsViewTestHooks();
|
|
|
|
|
|
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());
|
|
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,
|
|
running: false,
|
|
runtime_status: "failed",
|
|
runtime_error: "Channel dependencies could not be installed. Check gateway logs.",
|
|
installed: false,
|
|
ready: false,
|
|
status: "missing_dependency",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}],
|
|
enabled_count: 1,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
requestMutationMock.mockResolvedValueOnce({
|
|
features: [{
|
|
name: "matrix",
|
|
display_name: "Matrix",
|
|
type: "channel",
|
|
enabled: true,
|
|
installed: true,
|
|
ready: true,
|
|
status: "enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}],
|
|
enabled_count: 1,
|
|
last_action: { ok: true, message: "Enabled channel 'matrix'", enabled: true },
|
|
});
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
expect(await screen.findByRole("button", { name: "View Matrix settings" })).toBeInTheDocument();
|
|
expect(screen.queryByText("0 running · 1 channels")).not.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", "false");
|
|
fireEvent.click(screen.getByRole("button", { name: "Install support" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "Install and enable" }));
|
|
|
|
await waitFor(() =>
|
|
expect(requestMutationMock).toHaveBeenCalledWith(
|
|
"settings.feature.enable",
|
|
{ name: "matrix" },
|
|
150_000,
|
|
),
|
|
);
|
|
});
|
|
|
|
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.queryByText("0 running · 1 channels")).not.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);
|
|
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: false,
|
|
configured: false,
|
|
installed: true,
|
|
ready: false,
|
|
status: "not_enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}],
|
|
enabled_count: 0,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
requestMutationMock.mockResolvedValueOnce({
|
|
session_id: "feishu-session",
|
|
status: "pending",
|
|
qr_url: "https://accounts.feishu.cn/login?device_code=device",
|
|
domain: "feishu",
|
|
interval_ms: 5000,
|
|
expires_at_ms: Date.now() + 600_000,
|
|
message: "Scan with Feishu or Lark to connect.",
|
|
});
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
expect(await screen.findByRole("button", { name: "View Feishu settings" })).toBeInTheDocument();
|
|
expect(screen.queryByText("nanobot channels login feishu")).not.toBeInTheDocument();
|
|
fireEvent.click(await screen.findByRole(
|
|
"button",
|
|
{ name: "nanobot" },
|
|
{ timeout: 3_000 },
|
|
));
|
|
fireEvent.click(screen.getByRole("button", { name: "Connect" }));
|
|
|
|
await waitFor(() =>
|
|
expect(requestMutationMock).toHaveBeenCalledWith(
|
|
"settings.channel.connect.start",
|
|
{ channel: "feishu", domain: "feishu", instance_id: "default", mode: "replace" },
|
|
150_000,
|
|
),
|
|
);
|
|
expect(await screen.findByText("Scan with Feishu")).toBeInTheDocument();
|
|
expect(screen.getByText("Waiting for authorization...")).toBeInTheDocument();
|
|
});
|
|
|
|
it("starts Feishu connect from the default assistant action", 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: "feishu",
|
|
display_name: "Feishu",
|
|
webui: "webui/index.tsx",
|
|
type: "channel",
|
|
enabled: false,
|
|
configured: false,
|
|
installed: true,
|
|
ready: false,
|
|
status: "not_enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}],
|
|
enabled_count: 0,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
requestMutationMock.mockResolvedValueOnce({
|
|
session_id: "feishu-switch-session",
|
|
status: "pending",
|
|
qr_url: "https://accounts.feishu.cn/login?device_code=switch-device",
|
|
domain: "feishu",
|
|
interval_ms: 5000,
|
|
expires_at_ms: Date.now() + 600_000,
|
|
message: "Scan with Feishu or Lark to connect.",
|
|
});
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
expect(await screen.findByRole("button", { name: "View Feishu settings" })).toBeInTheDocument();
|
|
fireEvent.click(await screen.findByRole(
|
|
"button",
|
|
{ name: "nanobot" },
|
|
{ timeout: 3_000 },
|
|
));
|
|
fireEvent.click(screen.getByRole("button", { name: "Connect" }));
|
|
|
|
await waitFor(() =>
|
|
expect(requestMutationMock).toHaveBeenCalledWith(
|
|
"settings.channel.connect.start",
|
|
{ channel: "feishu", domain: "feishu", instance_id: "default", mode: "replace" },
|
|
150_000,
|
|
),
|
|
);
|
|
expect(await screen.findByText("Scan with Feishu")).toBeInTheDocument();
|
|
});
|
|
|
|
it("enables configured Feishu assistant without starting a new connect flow", 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: "feishu",
|
|
display_name: "Feishu",
|
|
webui: "webui/index.tsx",
|
|
type: "channel",
|
|
enabled: false,
|
|
configured: true,
|
|
installed: true,
|
|
ready: false,
|
|
status: "not_enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}],
|
|
enabled_count: 0,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
requestMutationMock.mockImplementation(async (action: string) => {
|
|
if (action === "settings.feature.enable") {
|
|
return {
|
|
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",
|
|
enabled: true,
|
|
running: true,
|
|
runtime_status: "running",
|
|
configured: true,
|
|
config_values: { "channels.feishu.appId": "cli_test" },
|
|
configured_fields: [
|
|
"channels.feishu.appId",
|
|
"channels.feishu.appSecret",
|
|
],
|
|
}],
|
|
installed: true,
|
|
ready: true,
|
|
status: "enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}],
|
|
enabled_count: 1,
|
|
requires_restart: false,
|
|
last_action: { ok: true, message: "Enabled channel 'feishu'", enabled: true },
|
|
};
|
|
}
|
|
if (action === "settings.channel.connect.start") {
|
|
throw new Error("Feishu connect should not start when credentials are already configured");
|
|
}
|
|
return settingsPayload();
|
|
});
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: "nanobot" }));
|
|
fireEvent.click(await screen.findByRole("switch", { name: "nanobot assistant" }));
|
|
|
|
await waitFor(() =>
|
|
expect(requestMutationMock).toHaveBeenCalledWith(
|
|
"settings.feature.enable",
|
|
{ name: "feishu", instance_id: "default" },
|
|
150_000,
|
|
),
|
|
);
|
|
expect(requestMutationMock.mock.calls.some(([action]) => (
|
|
action === "settings.channel.connect.start"
|
|
))).toBe(false);
|
|
expect(screen.getByRole("switch", { name: "nanobot assistant" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
});
|
|
|
|
it("shows Feishu assistant instances in the channel details", async () => {
|
|
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: true,
|
|
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",
|
|
enabled: true,
|
|
configured: true,
|
|
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",
|
|
enabled: false,
|
|
configured: true,
|
|
config_values: { "channels.feishu.appId": "cli_product" },
|
|
configured_fields: [
|
|
"channels.feishu.appId",
|
|
"channels.feishu.appSecret",
|
|
],
|
|
},
|
|
],
|
|
}],
|
|
enabled_count: 1,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
}),
|
|
);
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
expect(await screen.findByText("Product Helper")).toBeInTheDocument();
|
|
expect(screen.getAllByText("Support Bot")).toHaveLength(1);
|
|
expect(document.querySelector('img[src="https://example.com/support.png"]')).toBeTruthy();
|
|
|
|
expect(screen.getByRole("button", { name: /Support Bot/ })).toHaveAttribute(
|
|
"aria-expanded",
|
|
"false",
|
|
);
|
|
expect(screen.getByRole("button", { name: /Product Helper/ })).toHaveAttribute(
|
|
"aria-expanded",
|
|
"false",
|
|
);
|
|
|
|
expect(screen.queryByText("cli_def...ault")).not.toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("button", { name: /Support Bot/ }));
|
|
expect(screen.getByRole("button", { name: /Support Bot/ })).toHaveAttribute(
|
|
"aria-expanded",
|
|
"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(
|
|
"aria-expanded",
|
|
"false",
|
|
);
|
|
expect(screen.getByRole("button", { name: /Product Helper/ })).toHaveAttribute(
|
|
"aria-expanded",
|
|
"true",
|
|
);
|
|
});
|
|
|
|
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 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: [{
|
|
id: "default",
|
|
name: "nanobot",
|
|
display_name: "Support Bot",
|
|
avatar_url: "https://example.com/support.png",
|
|
enabled: true,
|
|
running: true,
|
|
runtime_status: "running",
|
|
configured: true,
|
|
config_values: { "channels.feishu.appId": "cli_support" },
|
|
configured_fields: [
|
|
"channels.feishu.appId",
|
|
"channels.feishu.appSecret",
|
|
],
|
|
}],
|
|
}],
|
|
enabled_count: 1,
|
|
};
|
|
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(feishuPayload);
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
}),
|
|
);
|
|
requestMutationMock.mockResolvedValueOnce(feishuPayload);
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
await screen.findByText("Support Bot");
|
|
expect(screen.getAllByText("Support Bot")).toHaveLength(1);
|
|
expect(screen.getByText("1 assistant connected")).toBeInTheDocument();
|
|
expect(screen.getByRole("switch", { name: "Support Bot assistant" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
expect(screen.queryByText("cli_sup...port")).not.toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("button", { name: "Support Bot" }));
|
|
expect(screen.getByText("cli_sup...port")).toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: "Replace assistant" })).not.toBeInTheDocument();
|
|
fireEvent.click(screen.getByRole("button", { name: "Reconnect" }));
|
|
await waitFor(() => expect(requestMutationMock).toHaveBeenCalledWith(
|
|
"settings.feature.enable",
|
|
{ name: "feishu", instance_id: "default" },
|
|
150_000,
|
|
));
|
|
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.queryByText("0 running · 1 channels")).not.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",
|
|
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: "discord",
|
|
display_name: "Discord",
|
|
webui: "webui/index.ts",
|
|
type: "channel",
|
|
enabled: true,
|
|
installed: true,
|
|
ready: true,
|
|
status: "enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
setup: channelSetupContract("discord"),
|
|
}],
|
|
enabled_count: 1,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
}),
|
|
);
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
expect(await screen.findByRole("button", { name: "View Discord settings" })).toBeInTheDocument();
|
|
fireEvent.click(screen.getByText("Advanced"));
|
|
|
|
const behavior = screen.getByRole("radiogroup", { name: "Group behavior" });
|
|
expect(within(behavior).getByRole("radio", { name: "Mention only" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
expect(within(behavior).getByRole("radio", { name: "All messages" })).toBeInTheDocument();
|
|
expect(screen.queryByPlaceholderText("mention")).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(within(behavior).getByRole("radio", { name: "All messages" }));
|
|
|
|
expect(within(behavior).getByRole("radio", { name: "All messages" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
});
|
|
|
|
it("uses a list-to-detail navigation stack on compact screens", async () => {
|
|
vi.stubGlobal(
|
|
"matchMedia",
|
|
vi.fn((query: string) => ({
|
|
matches: false,
|
|
media: query,
|
|
onchange: null,
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
addListener: vi.fn(),
|
|
removeListener: vi.fn(),
|
|
dispatchEvent: vi.fn(),
|
|
})),
|
|
);
|
|
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: "email",
|
|
display_name: "Email",
|
|
type: "channel",
|
|
enabled: false,
|
|
installed: true,
|
|
ready: false,
|
|
status: "not_enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}],
|
|
enabled_count: 0,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
}),
|
|
);
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
const emailRow = await screen.findByRole("button", { name: "View Email settings" });
|
|
expect(screen.getByPlaceholderText("Search channels")).toHaveClass(
|
|
"focus-visible:ring-2",
|
|
"focus-visible:ring-inset",
|
|
"focus-visible:ring-ring/50",
|
|
);
|
|
expect(screen.queryByRole("switch", { name: "Email channel" })).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(emailRow);
|
|
|
|
expect(screen.getByRole("button", { name: "All channels" })).toBeInTheDocument();
|
|
expect(screen.getByRole("switch", { name: "Email channel" })).toBeInTheDocument();
|
|
expect(screen.queryByPlaceholderText("Search channels")).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "All channels" }));
|
|
|
|
expect(screen.getByPlaceholderText("Search channels")).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: "View Email settings" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("saves Discord credentials from the channel setup panel", 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: "discord",
|
|
display_name: "Discord",
|
|
webui: "webui/index.ts",
|
|
type: "channel",
|
|
enabled: false,
|
|
configured: false,
|
|
installed: true,
|
|
ready: false,
|
|
status: "not_enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
setup: channelSetupContract("discord"),
|
|
}],
|
|
enabled_count: 0,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
requestMutationMock
|
|
.mockResolvedValueOnce({
|
|
name: "discord",
|
|
status: "ready",
|
|
checks: [],
|
|
missing_fields: [],
|
|
can_enable: true,
|
|
requires_restart: false,
|
|
})
|
|
.mockResolvedValueOnce({
|
|
name: "discord",
|
|
saved: true,
|
|
saved_keys: [
|
|
"channels.discord.token",
|
|
"channels.discord.allowChannels",
|
|
"channels.discord.groupPolicy",
|
|
],
|
|
nanobot_features: {
|
|
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,
|
|
},
|
|
});
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
expect(await screen.findByRole("button", { name: "View Discord settings" })).toBeInTheDocument();
|
|
expect(screen.getByRole("link", { name: "Open Discord setup" })).toHaveAttribute(
|
|
"href",
|
|
"https://nanobot.wiki/docs/0.2.2/getting-started/chat-apps#discord",
|
|
);
|
|
expect(screen.getByRole("switch", { name: "Discord channel" })).toBeDisabled();
|
|
fireEvent.change(screen.getByPlaceholderText("Discord bot token"), {
|
|
target: { value: "discord-token" },
|
|
});
|
|
fireEvent.click(screen.getByText("Advanced"));
|
|
fireEvent.change(screen.getByLabelText("Allowed channels"), {
|
|
target: { value: "123, 456" },
|
|
});
|
|
fireEvent.click(within(screen.getByRole("radiogroup", { name: "Group behavior" })).getByRole(
|
|
"radio",
|
|
{ name: "All messages" },
|
|
));
|
|
fireEvent.click(screen.getByRole("button", { name: "Check and enable" }));
|
|
|
|
await waitFor(() =>
|
|
expect(
|
|
requestMutationMock.mock.calls.some(([action]) => (
|
|
action === "settings.channel.configure"
|
|
)),
|
|
).toBe(true),
|
|
);
|
|
expect(requestMutationMock).toHaveBeenCalledWith(
|
|
"settings.channel.configure",
|
|
{
|
|
name: "discord",
|
|
enable: true,
|
|
values: {
|
|
"channels.discord.token": "discord-token",
|
|
"channels.discord.allowChannels": "123, 456",
|
|
"channels.discord.groupPolicy": "open",
|
|
},
|
|
},
|
|
150_000,
|
|
);
|
|
expect(await screen.findByText("Checked and enabled.")).toBeInTheDocument();
|
|
expect(screen.getByRole("switch", { name: "Discord channel" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
});
|
|
|
|
it("prefills saved channel config without exposing secrets", 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: "discord",
|
|
display_name: "Discord",
|
|
webui: "webui/index.ts",
|
|
type: "channel",
|
|
enabled: false,
|
|
configured: true,
|
|
installed: true,
|
|
ready: false,
|
|
status: "not_enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
config_values: {
|
|
"channels.discord.allowChannels": "123, 456",
|
|
"channels.discord.groupPolicy": "open",
|
|
},
|
|
configured_fields: [
|
|
"channels.discord.token",
|
|
"channels.discord.allowChannels",
|
|
"channels.discord.groupPolicy",
|
|
],
|
|
setup: channelSetupContract("discord"),
|
|
}],
|
|
enabled_count: 0,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
requestMutationMock.mockResolvedValueOnce({
|
|
features: [{
|
|
name: "discord",
|
|
display_name: "Discord",
|
|
webui: "webui/index.ts",
|
|
type: "channel",
|
|
enabled: true,
|
|
configured: true,
|
|
installed: true,
|
|
ready: true,
|
|
status: "enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
setup: channelSetupContract("discord"),
|
|
}],
|
|
enabled_count: 1,
|
|
requires_restart: false,
|
|
});
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
expect(await screen.findByRole("button", { name: "View Discord settings" })).toBeInTheDocument();
|
|
expect(screen.getByRole("switch", { name: "Discord channel" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"false",
|
|
);
|
|
expect(screen.getByRole("switch", { name: "Discord channel" })).toBeEnabled();
|
|
expect(screen.getByText("Configured manually")).toBeInTheDocument();
|
|
expect(screen.getByText("Saved")).toBeInTheDocument();
|
|
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"));
|
|
expect(screen.getByLabelText("Allowed channels")).toHaveValue("123, 456");
|
|
expect(within(screen.getByRole("radiogroup", { name: "Group behavior" })).getByRole(
|
|
"radio",
|
|
{ name: "All messages" },
|
|
)).toHaveAttribute("aria-checked", "true");
|
|
|
|
fireEvent.click(screen.getByRole("switch", { name: "Discord channel" }));
|
|
await waitFor(() =>
|
|
expect(requestMutationMock).toHaveBeenCalledWith(
|
|
"settings.feature.enable",
|
|
{ name: "discord" },
|
|
150_000,
|
|
),
|
|
);
|
|
});
|
|
|
|
it("shows an actionable credential guide for Telegram", async () => {
|
|
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: "telegram",
|
|
display_name: "Telegram",
|
|
webui: "webui/index.ts",
|
|
type: "channel",
|
|
enabled: false,
|
|
installed: true,
|
|
ready: false,
|
|
status: "not_enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}],
|
|
enabled_count: 0,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
}),
|
|
);
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
expect(await screen.findByRole("button", { name: "View Telegram settings" })).toBeInTheDocument();
|
|
expect(screen.getByRole("link", { name: "Open Telegram setup" })).toHaveAttribute(
|
|
"href",
|
|
"https://nanobot.wiki/docs/0.2.2/getting-started/chat-apps#telegram",
|
|
);
|
|
});
|
|
|
|
it("shows branded setup guide links for supported WebUI channels", async () => {
|
|
const channels = [
|
|
["websocket", "WebSocket", "Open WebSocket setup"],
|
|
["telegram", "Telegram", "Open Telegram setup"],
|
|
["feishu", "Feishu", "Open Feishu setup"],
|
|
["slack", "Slack", "Open Slack setup"],
|
|
["discord", "Discord", "Open Discord setup"],
|
|
["email", "Email", "Open Email setup"],
|
|
["matrix", "Matrix", "Open Matrix setup"],
|
|
["whatsapp", "WhatsApp", "Open WhatsApp setup"],
|
|
["dingtalk", "DingTalk", "Open DingTalk setup"],
|
|
["wecom", "WeCom", "Open WeCom setup"],
|
|
["weixin", "WeChat", "Open WeChat setup"],
|
|
["qq", "QQ", "Open QQ setup"],
|
|
["signal", "Signal", "Open Signal setup"],
|
|
["msteams", "Microsoft Teams", "Open Teams setup"],
|
|
["napcat", "NapCat", "Open NapCat setup"],
|
|
] as const;
|
|
const hiddenChannels = [["mochat", "MoChat"]] as const;
|
|
|
|
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: 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,
|
|
ready: name === "websocket",
|
|
status: name === "websocket" ? "enabled" : "not_enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
})).concat(hiddenChannels.map(([name, displayName]) => ({
|
|
name,
|
|
display_name: displayName,
|
|
settings_visible: false,
|
|
type: "channel",
|
|
enabled: false,
|
|
installed: true,
|
|
ready: false,
|
|
status: "not_enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}))),
|
|
enabled_count: 1,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
}),
|
|
);
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
for (const [, displayName, guideLabel] of channels) {
|
|
fireEvent.click(await screen.findByRole("button", { name: `View ${displayName} settings` }));
|
|
if (displayName === "Feishu") {
|
|
fireEvent.click(await screen.findByRole(
|
|
"button",
|
|
{ name: "nanobot" },
|
|
{ timeout: 3_000 },
|
|
));
|
|
}
|
|
const guide = await screen.findByRole("link", { name: guideLabel });
|
|
expect(guide).toHaveAttribute("href", expect.stringMatching(/^https:\/\//));
|
|
expect(guide.querySelector("span[aria-hidden] img, span[aria-hidden] svg")).not.toBeNull();
|
|
}
|
|
expect(screen.queryByRole("button", { name: "View MoChat settings" })).not.toBeInTheDocument();
|
|
});
|
|
|
|
it("uses choices for channel enum and boolean fields", async () => {
|
|
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: ["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,
|
|
ready: true,
|
|
status: "enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
setup: channelSetupContract(name as "email" | "feishu" | "matrix" | "qq"),
|
|
})),
|
|
enabled_count: 4,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
}),
|
|
);
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: "View Email settings" }));
|
|
const consent = screen.getByRole("radiogroup", { name: "Consent granted" });
|
|
expect(within(consent).getByRole("radio", { name: "Not granted" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
expect(within(consent).getByRole("radio", { name: "Granted" })).toBeInTheDocument();
|
|
expect(screen.queryByPlaceholderText("true")).not.toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "View Feishu settings" }));
|
|
fireEvent.click(screen.getByRole("button", { name: "nanobot" }));
|
|
fireEvent.click(screen.getByText("Advanced"));
|
|
const region = screen.getByRole("radiogroup", { name: "Region" });
|
|
expect(within(region).getByRole("radio", { name: "Feishu" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
expect(within(region).getByRole("radio", { name: "Lark" })).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "View Matrix settings" }));
|
|
fireEvent.click(screen.getByText("Advanced"));
|
|
const matrixBehavior = screen.getByRole("radiogroup", { name: "Group behavior" });
|
|
expect(within(matrixBehavior).getByRole("radio", { name: "All messages" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
expect(within(matrixBehavior).getByRole("radio", { name: "Allowlist" })).toBeInTheDocument();
|
|
|
|
fireEvent.click(screen.getByRole("button", { name: "View QQ settings" }));
|
|
fireEvent.click(screen.getByText("Advanced"));
|
|
const format = screen.getByRole("radiogroup", { name: "Message format" });
|
|
expect(within(format).getByRole("radio", { name: "Plain text" })).toHaveAttribute(
|
|
"aria-checked",
|
|
"true",
|
|
);
|
|
expect(within(format).getByRole("radio", { name: "Markdown" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("does not offer to disable the websocket channel", 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: "websocket",
|
|
display_name: "Websocket",
|
|
capabilities: ["always_enabled"],
|
|
webui: "webui/index.ts",
|
|
type: "channel",
|
|
enabled: true,
|
|
installed: true,
|
|
ready: true,
|
|
status: "enabled",
|
|
install_supported: true,
|
|
requires_restart: true,
|
|
}],
|
|
enabled_count: 1,
|
|
});
|
|
}
|
|
return { ok: false, status: 404, json: async () => ({}) } as Response;
|
|
});
|
|
vi.stubGlobal("fetch", fetchMock);
|
|
|
|
renderSettingsView({ initialSection: "channels" });
|
|
|
|
expect(await screen.findByRole("button", { name: "View WebSocket settings" })).toBeInTheDocument();
|
|
expect(screen.getAllByText("WebSocket")).toHaveLength(2);
|
|
expect(screen.queryByText("Required for WebUI")).not.toBeInTheDocument();
|
|
expect(screen.getAllByText("On").length).toBeGreaterThanOrEqual(1);
|
|
expect(screen.getByText("Managed by WebUI")).toBeInTheDocument();
|
|
expect(screen.queryByText("Configured manually")).not.toBeInTheDocument();
|
|
expect(screen.queryByRole("button", { name: "Disable channel" })).not.toBeInTheDocument();
|
|
const websocketSwitch = screen.getByRole("switch", { name: "WebSocket channel" });
|
|
expect(websocketSwitch).toBeDisabled();
|
|
expect(websocketSwitch).toHaveAttribute("aria-checked", "true");
|
|
expect(requestMutationMock).not.toHaveBeenCalled();
|
|
});
|
|
});
|