From 1272311ebe994ed89490cd6399cb986169cfeb99 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Tue, 16 Jun 2026 17:23:35 +0800 Subject: [PATCH] Revert "MUL-3312: gate chat uploads on active agent (#4192)" (#4195) This reverts commit 097064ed0e7268420e1ff99b5ececdd04b380598. --- .../views/chat/components/chat-input.test.tsx | 1 - .../views/chat/components/chat-window.tsx | 2 +- .../chat-window.upload-readiness.test.tsx | 360 ------------------ 3 files changed, 1 insertion(+), 362 deletions(-) delete mode 100644 packages/views/chat/components/chat-window.upload-readiness.test.tsx diff --git a/packages/views/chat/components/chat-input.test.tsx b/packages/views/chat/components/chat-input.test.tsx index 5342c16bd..ee123e815 100644 --- a/packages/views/chat/components/chat-input.test.tsx +++ b/packages/views/chat/components/chat-input.test.tsx @@ -321,7 +321,6 @@ describe("ChatInput attachment wiring", () => { it("does not render the file upload button when onUploadFile is omitted", () => { renderInput({ onUploadFile: undefined }); - expect(editorProps.last?.onUploadFile).toBeUndefined(); // FileUploadButton renders an icon button labelled by its tooltip — when // upload wiring is absent the chat input falls back to "submit + extras" // only. Probe by counting buttons: with no upload, only the submit diff --git a/packages/views/chat/components/chat-window.tsx b/packages/views/chat/components/chat-window.tsx index 1bb9cf18d..703c8d0ac 100644 --- a/packages/views/chat/components/chat-window.tsx +++ b/packages/views/chat/components/chat-window.tsx @@ -770,7 +770,7 @@ export function ChatWindow() { onSend={handleSend} restoreDraftRequest={restoreDraftRequest} onRestoreDraftConsumed={handleRestoreDraftConsumed} - onUploadFile={activeAgent ? handleUploadFile : undefined} + onUploadFile={handleUploadFile} onStop={handleStop} isRunning={!!pendingTaskId} disabled={isSessionArchived} diff --git a/packages/views/chat/components/chat-window.upload-readiness.test.tsx b/packages/views/chat/components/chat-window.upload-readiness.test.tsx deleted file mode 100644 index 4c47fd8e9..000000000 --- a/packages/views/chat/components/chat-window.upload-readiness.test.tsx +++ /dev/null @@ -1,360 +0,0 @@ -import { describe, expect, it, beforeEach, vi } from "vitest"; -import { fireEvent, render, screen, waitFor } from "@testing-library/react"; -import { I18nProvider } from "@multica/core/i18n/react"; -import type { Agent, ChatSession } from "@multica/core/types"; -import type { UploadResult } from "@multica/core/hooks/use-file-upload"; -import enCommon from "../../locales/en/common.json"; -import enChat from "../../locales/en/chat.json"; -import enIssues from "../../locales/en/issues.json"; - -const mocks = vi.hoisted(() => { - const chatState = { - isOpen: true, - isExpanded: false, - activeSessionId: null as string | null, - selectedAgentId: null as string | null, - setOpen: vi.fn(), - setActiveSession: vi.fn(), - setSelectedAgentId: vi.fn(), - }; - - return { - queryState: { - agents: [] as unknown[], - members: [] as unknown[], - sessions: [] as unknown[], - pendingTask: null as unknown, - availability: "loading", - }, - chatState, - chatInput: { - lastProps: null as null | Record, - fileCardsInserted: 0, - }, - queryClient: { - setQueryData: vi.fn(), - invalidateQueries: vi.fn(), - cancelQueries: vi.fn(), - }, - createSession: vi.fn(), - deleteSession: { - mutate: vi.fn(), - isPending: false, - }, - markRead: { - mutate: vi.fn(), - }, - updateSession: { - mutate: vi.fn(), - isPending: false, - }, - uploadWithToast: vi.fn(), - fetchOlderMessages: vi.fn(), - }; -}); - -vi.mock("motion/react", () => ({ - motion: { - div: ({ children, initial: _initial, animate: _animate, transition: _transition, ...props }: any) => ( -
{children}
- ), - }, -})); - -vi.mock("sonner", () => ({ - toast: { - error: vi.fn(), - }, -})); - -vi.mock("@tanstack/react-query", () => ({ - queryOptions: (options: unknown) => options, - infiniteQueryOptions: (options: unknown) => options, - useQueryClient: () => mocks.queryClient, - useQuery: (options: { queryKey?: readonly unknown[] }) => { - const key = JSON.stringify(options.queryKey ?? []); - if (key.includes('"agents"')) return { data: mocks.queryState.agents }; - if (key.includes('"members"')) return { data: mocks.queryState.members }; - if (key.includes('"sessions"')) return { data: mocks.queryState.sessions }; - if (key.includes('"pending-tasks"')) return { data: { tasks: [] } }; - if (key.includes('"pending-task"')) return { data: mocks.queryState.pendingTask }; - return { data: undefined }; - }, - useInfiniteQuery: () => ({ - data: { - pages: [{ - messages: [], - limit: 50, - has_more: false, - next_cursor: null, - }], - pageParams: [null], - }, - isLoading: false, - fetchNextPage: mocks.fetchOlderMessages, - hasNextPage: false, - isFetchingNextPage: false, - }), -})); - -vi.mock("@multica/core/hooks", () => ({ - useWorkspaceId: () => "ws-1", -})); - -vi.mock("@multica/core/auth", () => ({ - useAuthStore: (selector: (state: { user: { id: string } }) => unknown) => - selector({ user: { id: "user-1" } }), -})); - -vi.mock("@multica/core/agents", () => ({ - useAgentPresenceDetail: () => "loading", - useWorkspaceAgentAvailability: () => mocks.queryState.availability, -})); - -vi.mock("@multica/core/hooks/use-file-upload", () => ({ - useFileUpload: () => ({ - uploadWithToast: mocks.uploadWithToast, - }), -})); - -vi.mock("@multica/core/chat", () => ({ - useChatStore: (selector: (state: typeof mocks.chatState) => unknown) => - selector(mocks.chatState), -})); - -vi.mock("@multica/core/chat/mutations", () => ({ - useCreateChatSession: () => ({ - mutateAsync: mocks.createSession, - }), - useDeleteChatSession: () => mocks.deleteSession, - useMarkChatSessionRead: () => mocks.markRead, - useUpdateChatSession: () => mocks.updateSession, -})); - -vi.mock("@multica/views/issues/components", () => ({ - canAssignAgent: () => true, -})); - -vi.mock("@multica/core/api", () => ({ - api: { - cancelTaskById: vi.fn(), - }, -})); - -vi.mock("../../common/actor-avatar", () => ({ - ActorAvatar: ({ actorId }: { actorId: string }) => ( - - ), -})); - -vi.mock("./chat-message-list", () => ({ - ChatMessageList: () =>
, - ChatMessageSkeleton: () =>
, -})); - -vi.mock("./chat-resize-handles", () => ({ - ChatResizeHandles: () => null, -})); - -vi.mock("./offline-banner", () => ({ - OfflineBanner: () => null, -})); - -vi.mock("./no-agent-banner", () => ({ - NoAgentBanner: () => null, -})); - -vi.mock("./use-chat-context-items", () => ({ - useChatContextItems: () => [], -})); - -vi.mock("./use-chat-resize", () => ({ - useChatResize: () => ({ - renderWidth: 420, - renderHeight: 520, - isAtMax: false, - boundsReady: true, - isDragging: false, - toggleExpand: vi.fn(), - startDrag: vi.fn(), - }), -})); - -vi.mock("./chat-input", () => ({ - ChatInput: (props: { - onUploadFile?: (file: File) => Promise; - }) => { - mocks.chatInput.lastProps = props as unknown as Record; - return ( - - ); - }, -})); - -import { ChatWindow } from "./chat-window"; - -const TEST_RESOURCES = { en: { common: enCommon, chat: enChat, issues: enIssues } }; - -function makeAgent(overrides: Partial & Pick): Agent { - return { - workspace_id: "ws-1", - runtime_id: "runtime-1", - description: "", - instructions: "", - avatar_url: null, - runtime_mode: "local", - runtime_config: {}, - custom_args: [], - visibility: "workspace", - status: "idle", - max_concurrent_tasks: 1, - model: "sonnet", - skills: [], - created_at: new Date(0).toISOString(), - updated_at: new Date(0).toISOString(), - archived_at: null, - archived_by: null, - ...overrides, - id: overrides.id, - name: overrides.name, - owner_id: overrides.owner_id, - }; -} - -function makeUpload(): UploadResult { - return { - id: "att-1", - workspace_id: "ws-1", - issue_id: null, - comment_id: null, - chat_session_id: "session-1", - chat_message_id: null, - uploader_type: "member", - uploader_id: "user-1", - filename: "brief.pdf", - url: "https://cdn.example/brief.pdf", - download_url: "https://cdn.example/brief.pdf", - markdown_url: "/api/attachments/att-1/download", - content_type: "application/pdf", - size_bytes: 3, - created_at: new Date(0).toISOString(), - link: "https://cdn.example/brief.pdf", - markdownLink: "/api/attachments/att-1/download", - }; -} - -function makeSession(overrides: Partial = {}): ChatSession { - return { - id: "session-1", - workspace_id: "ws-1", - agent_id: "agent-1", - creator_id: "user-1", - title: "", - status: "active", - has_unread: false, - created_at: new Date(0).toISOString(), - updated_at: new Date(0).toISOString(), - ...overrides, - }; -} - -function renderChatWindow() { - return render( - - - , - ); -} - -beforeEach(() => { - mocks.queryState.agents = []; - mocks.queryState.members = [{ user_id: "user-1", role: "owner" }]; - mocks.queryState.sessions = []; - mocks.queryState.pendingTask = null; - mocks.queryState.availability = "loading"; - mocks.chatState.isOpen = true; - mocks.chatState.isExpanded = false; - mocks.chatState.activeSessionId = null; - mocks.chatState.selectedAgentId = null; - mocks.chatInput.lastProps = null; - mocks.chatInput.fileCardsInserted = 0; - mocks.queryClient.setQueryData.mockClear(); - mocks.queryClient.invalidateQueries.mockClear(); - mocks.queryClient.cancelQueries.mockClear(); - mocks.createSession.mockReset(); - mocks.createSession.mockResolvedValue(makeSession()); - mocks.deleteSession.mutate.mockClear(); - mocks.markRead.mutate.mockClear(); - mocks.updateSession.mutate.mockClear(); - mocks.uploadWithToast.mockReset(); - mocks.uploadWithToast.mockResolvedValue(makeUpload()); - mocks.fetchOlderMessages.mockClear(); - mocks.chatState.setOpen.mockClear(); - mocks.chatState.setActiveSession.mockClear(); - mocks.chatState.setSelectedAgentId.mockClear(); -}); - -describe("ChatWindow upload readiness", () => { - it("does not expose PDF upload while activeAgent is unavailable", async () => { - renderChatWindow(); - - const uploadButton = await screen.findByTestId("mock-chat-upload"); - expect(uploadButton).toBeDisabled(); - expect(mocks.chatInput.lastProps?.onUploadFile).toBeUndefined(); - - fireEvent.click(uploadButton); - - expect(mocks.chatInput.fileCardsInserted).toBe(0); - expect(mocks.createSession).not.toHaveBeenCalled(); - expect(mocks.uploadWithToast).not.toHaveBeenCalled(); - }); - - it("keeps PDF upload available after an activeAgent resolves", async () => { - const view = renderChatWindow(); - - expect(await screen.findByTestId("mock-chat-upload")).toBeDisabled(); - - mocks.queryState.agents = [ - makeAgent({ id: "agent-1", name: "Multica", owner_id: "user-1" }), - ]; - mocks.queryState.availability = "available"; - view.rerender( - - - , - ); - - const uploadButton = await screen.findByTestId("mock-chat-upload"); - await waitFor(() => { - expect(uploadButton).not.toBeDisabled(); - expect(mocks.chatInput.lastProps?.onUploadFile).toEqual(expect.any(Function)); - }); - - fireEvent.click(uploadButton); - - await waitFor(() => { - expect(mocks.createSession).toHaveBeenCalledWith({ - agent_id: "agent-1", - title: "", - }); - }); - expect(mocks.uploadWithToast).toHaveBeenCalledWith( - expect.any(File), - { chatSessionId: "session-1" }, - ); - expect(mocks.chatState.setActiveSession).toHaveBeenCalledWith("session-1"); - }); -});