mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-28 22:20:35 +00:00
* fix(channels): keep IM binding codes on screen and report goal state in replies Connecting Slack, Discord, Feishu, DingTalk, WeCom, Buzz or QQ now opens a dialog that keeps the /connect command on screen with a copy button, its expiry time and the binding status, instead of a toast that disappears. The bots' /connect replies and the IM /goal replies now use the owner's web UI language (else channel_connections.notification_locale), and /goal reports the goal's state as the web goal bar does: in progress, why it stopped, or that it was met. * docs(changelog): note the IM binding dialog and localized IM replies (#7591) * test(channels): check IM reply tables against every notification locale The connect-reply check compared a fixed en-US/zh-CN pair, and the /goal reply table had none. Both now match NOTIFICATION_LOCALES with the same keys in each, like the existing _GOAL_REASON_TEXT check, so a new locale fails here instead of raising KeyError at reply time. --------- Co-authored-by: Totoro-qaq <279883115+Totoro-qaq@users.noreply.github.com>
122 lines
3.9 KiB
TypeScript
122 lines
3.9 KiB
TypeScript
import { afterEach, beforeEach, expect, it, rs } from "@rstest/core";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { act, cleanup, renderHook } from "@testing-library/react";
|
|
import { StrictMode, type ReactNode } from "react";
|
|
|
|
rs.mock("@/core/channels/api", () => ({
|
|
connectChannelProvider: rs.fn(),
|
|
listChannelProviders: rs.fn(),
|
|
listChannelConnections: rs.fn(),
|
|
}));
|
|
|
|
import {
|
|
connectChannelProvider,
|
|
listChannelConnections,
|
|
listChannelProviders,
|
|
} from "@/core/channels/api";
|
|
import type {
|
|
ChannelConnectResponse,
|
|
ChannelConnection,
|
|
ChannelProvider,
|
|
} from "@/core/channels/types";
|
|
import { useBindingCode } from "@/core/channels/use-binding-code";
|
|
|
|
function minted(code: string, expiresIn = 600): ChannelConnectResponse {
|
|
return {
|
|
provider: "discord",
|
|
mode: "binding_code",
|
|
code,
|
|
instruction: `Send /connect ${code}`,
|
|
expires_in: expiresIn,
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
rs.useFakeTimers({ toFake: ["setTimeout", "clearTimeout", "Date"] });
|
|
rs.mocked(listChannelProviders).mockResolvedValue({
|
|
enabled: true,
|
|
providers: [],
|
|
});
|
|
rs.mocked(listChannelConnections).mockResolvedValue([]);
|
|
rs.mocked(connectChannelProvider).mockResolvedValue(minted("abc123"));
|
|
});
|
|
afterEach(() => {
|
|
cleanup();
|
|
rs.useRealTimers();
|
|
rs.resetAllMocks();
|
|
});
|
|
|
|
function mount(provider = "discord") {
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: false } },
|
|
});
|
|
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
<StrictMode>
|
|
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
</StrictMode>
|
|
);
|
|
return renderHook(() => useBindingCode({ provider }), { wrapper });
|
|
}
|
|
|
|
it("binds the requested provider and ignores other apps' connections", async () => {
|
|
const { result } = mount();
|
|
await act(() => rs.advanceTimersByTimeAsync(1));
|
|
expect(connectChannelProvider).toHaveBeenCalledExactlyOnceWith("discord");
|
|
expect(result.current.stage).toBe("binding");
|
|
expect(result.current.command).toBe("/connect abc123");
|
|
|
|
rs.mocked(listChannelConnections).mockResolvedValue([
|
|
{ provider: "slack", status: "connected" } as ChannelConnection,
|
|
]);
|
|
await act(() => rs.advanceTimersByTimeAsync(2000));
|
|
expect(result.current.stage).toBe("binding");
|
|
|
|
rs.mocked(listChannelConnections).mockResolvedValue([
|
|
{ provider: "discord", status: "connected" } as ChannelConnection,
|
|
]);
|
|
await act(() => rs.advanceTimersByTimeAsync(2000));
|
|
expect(result.current.stage).toBe("connected");
|
|
});
|
|
|
|
it("mints no code when the provider already reports a connection", async () => {
|
|
rs.mocked(listChannelProviders).mockResolvedValue({
|
|
enabled: true,
|
|
providers: [
|
|
{
|
|
provider: "discord",
|
|
connection_status: "connected",
|
|
} as ChannelProvider,
|
|
],
|
|
});
|
|
const { result } = mount();
|
|
await act(() => rs.advanceTimersByTimeAsync(1));
|
|
expect(result.current.stage).toBe("connected");
|
|
expect(connectChannelProvider).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stops polling when the caller unmounts", async () => {
|
|
const { unmount } = mount();
|
|
await act(() => rs.advanceTimersByTimeAsync(2000));
|
|
expect(listChannelConnections).toHaveBeenCalledTimes(1);
|
|
unmount();
|
|
await act(() => rs.advanceTimersByTimeAsync(10_000));
|
|
expect(listChannelConnections).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
it("starts every new code uncopied", async () => {
|
|
rs.mocked(connectChannelProvider)
|
|
.mockResolvedValueOnce(minted("first", 60))
|
|
.mockResolvedValueOnce(minted("second"));
|
|
const { result } = mount();
|
|
await act(() => rs.advanceTimersByTimeAsync(1));
|
|
act(() => result.current.reportCopy(true));
|
|
expect(result.current.copied).toBe(true);
|
|
|
|
await act(() => rs.advanceTimersByTimeAsync(60_000));
|
|
expect(result.current.stage).toBe("expired");
|
|
act(() => result.current.retry());
|
|
await act(() => rs.advanceTimersByTimeAsync(1));
|
|
expect(result.current.command).toBe("/connect second");
|
|
expect(result.current.copied).toBe(false);
|
|
});
|