feat: add permission request UI
This commit is contained in:
@@ -3,12 +3,18 @@
|
||||
import { act, renderHook, waitFor } from "@testing-library/react";
|
||||
|
||||
import { useAgentChatSession } from "./useAgentChatSession";
|
||||
import { abortAgentChat, resumeAgentChatStream, streamAgentChat } from "@/lib/chatStream";
|
||||
import {
|
||||
abortAgentChat,
|
||||
replyAgentPermission,
|
||||
resumeAgentChatStream,
|
||||
streamAgentChat,
|
||||
} from "@/lib/chatStream";
|
||||
import type { StreamEvent } from "@/lib/chatStream";
|
||||
|
||||
jest.mock("@/lib/chatStream", () => ({
|
||||
abortAgentChat: jest.fn(async () => undefined),
|
||||
forkAgentChat: jest.fn(async () => "forked-session"),
|
||||
replyAgentPermission: jest.fn(async () => undefined),
|
||||
resumeAgentChatStream: jest.fn(async () => undefined),
|
||||
streamAgentChat: jest.fn(async () => undefined),
|
||||
}));
|
||||
@@ -46,9 +52,11 @@ describe("useAgentChatSession", () => {
|
||||
saveActiveChatState.mockReset();
|
||||
updateChatSessionTitle.mockReset();
|
||||
jest.mocked(abortAgentChat).mockReset();
|
||||
jest.mocked(replyAgentPermission).mockReset();
|
||||
jest.mocked(resumeAgentChatStream).mockReset();
|
||||
jest.mocked(streamAgentChat).mockReset();
|
||||
jest.mocked(abortAgentChat).mockImplementation(async () => undefined);
|
||||
jest.mocked(replyAgentPermission).mockImplementation(async () => undefined);
|
||||
jest.mocked(resumeAgentChatStream).mockImplementation(async () => undefined);
|
||||
jest.mocked(streamAgentChat).mockImplementation(async () => undefined);
|
||||
deleteChatSession.mockImplementation(async () => undefined);
|
||||
@@ -353,6 +361,62 @@ describe("useAgentChatSession", () => {
|
||||
expect(abortAgentChat).toHaveBeenCalledWith("session-loaded");
|
||||
});
|
||||
|
||||
it("tracks permission requests and submits replies", async () => {
|
||||
listChatSessions.mockResolvedValue([]);
|
||||
let emitStreamEvent: ((event: StreamEvent) => void) | undefined;
|
||||
jest.mocked(streamAgentChat).mockImplementationOnce(async ({ onEvent }) => {
|
||||
emitStreamEvent = onEvent;
|
||||
await new Promise<void>(() => undefined);
|
||||
});
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
useAgentChatSession({
|
||||
projectId: "project-1",
|
||||
onToolCall: jest.fn(),
|
||||
}),
|
||||
);
|
||||
|
||||
await waitFor(() => expect(result.current.isHydrating).toBe(false));
|
||||
|
||||
await act(async () => {
|
||||
void result.current.sendPrompt("删除临时文件");
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
act(() => {
|
||||
emitStreamEvent?.({
|
||||
type: "permission_request",
|
||||
sessionId: "session-1",
|
||||
requestId: "perm-1",
|
||||
permission: "bash",
|
||||
patterns: ["rm *"],
|
||||
metadata: { command: "rm tmp.txt" },
|
||||
always: ["rm *"],
|
||||
createdAt: 123,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.messages.at(-1)?.permissions).toEqual([
|
||||
expect.objectContaining({
|
||||
requestId: "perm-1",
|
||||
sessionId: "session-1",
|
||||
status: "pending",
|
||||
}),
|
||||
]);
|
||||
|
||||
await act(async () => {
|
||||
await result.current.replyPermission("perm-1", "once");
|
||||
});
|
||||
|
||||
expect(replyAgentPermission).toHaveBeenCalledWith("session-1", "perm-1", "once");
|
||||
expect(result.current.messages.at(-1)?.permissions?.[0]).toEqual(
|
||||
expect.objectContaining({
|
||||
requestId: "perm-1",
|
||||
status: "approved_once",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("finalizes running progress when aborting an active prompt", async () => {
|
||||
listChatSessions.mockResolvedValue([]);
|
||||
jest.mocked(streamAgentChat).mockImplementationOnce(
|
||||
@@ -368,7 +432,7 @@ describe("useAgentChatSession", () => {
|
||||
startedAt: 1000,
|
||||
} satisfies StreamEvent);
|
||||
|
||||
signal.addEventListener("abort", () => {
|
||||
signal?.addEventListener("abort", () => {
|
||||
reject(new Error("aborted"));
|
||||
});
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user