Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

763 lines
24 KiB
TypeScript

import { fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { TooltipProvider } from "@tutti-os/ui-system";
import type { AgentQuickPromptLabels } from "./agentQuickPromptLabels";
import { AgentQuickPromptPopover } from "./AgentQuickPromptPopover";
import type { AgentQuickPromptLibraryController } from "./useAgentQuickPromptLibrary";
const labels = new Proxy(
{
deleteDescription: (title: string) => `Delete ${title}`,
dragCancel: (title: string) => `Canceled ${title}`,
dragDrop: (title: string) => `Dropped ${title}`,
dragHandle: (title: string) => `Reorder ${title}`,
dragMove: (title: string) => `Moving ${title}`,
dragStart: (title: string) => `Picked up ${title}`,
title: "Quick prompts",
trigger: "Prompts",
triggerTooltip: "Choose a quick prompt",
searchPlaceholder: "Search quick prompts",
startSorting: "Reorder",
finishSorting: "Done",
insertionError:
"Could not insert into the Composer. Try again from the quick prompt list.",
reorderDisabledMinimum:
"At least two quick prompts are required to adjust their order",
reorderDisabledPending:
"Wait for the current quick prompt change to finish",
reorderDisabledSearch: "Clear the search to adjust prompt order",
reorderDisabledUnsupported: "This host does not support prompt reordering",
add: "New prompt",
createFromTemplate: "Recommended templates",
moreActions: "More prompt actions",
edit: "Edit",
delete: "Delete",
empty: "No quick prompts yet",
loadError: "Quick prompts could not be loaded",
loading: "Loading quick prompts…",
noResults: "No matching quick prompts",
recommendedTemplatesTitle: "Recommended templates",
recommendedTemplatesDescription:
"Choose one to prefill the editor. Saving adds it as a quick prompt and inserts it into the Composer.",
returnToPrompts: "My prompts",
useTemplate: "Use template",
recommendedTemplates: [
{
id: "summary-common-prompts",
title: "Summarize common prompts",
description: "Find insights and repeated work patterns",
content: "Summarize my common prompts"
},
{
id: "understand-context",
title: "Understand the situation",
description: "Summarize context, constraints, risks, and next steps",
content: "Summarize the situation"
},
{
id: "create-action-plan",
title: "Create an action plan",
description: "Break a goal into prioritized, verifiable steps",
content: "Create an action plan"
},
{
id: "review-and-improve",
title: "Review and improve",
description: "Find gaps, risks, and practical improvements",
content: "Review and improve"
},
{
id: "draft-clear-update",
title: "Draft a clear update",
description: "Write a concise explanation for the intended audience",
content: "Draft an update"
}
]
},
{
get: (target, property) => Reflect.get(target, property) ?? String(property)
}
) as unknown as AgentQuickPromptLabels;
function controller(
patch: Partial<AgentQuickPromptLibraryController> = {}
): AgentQuickPromptLibraryController {
const prompt = {
id: "prompt-1",
title: "Review",
content: "Review the current change",
version: 1,
createdAtUnixMs: 1,
updatedAtUnixMs: 2
};
return {
capabilityAvailable: true,
canReorder: false,
close: vi.fn(),
closeDialog: vi.fn(),
deletePrompt: vi.fn(),
filteredPrompts: [prompt],
isDeleting: false,
isEditorOpen: false,
isPopoverOpen: true,
isSaving: false,
isInteractionLocked: false,
isReordering: false,
initialDraft: null,
insertionError: false,
labels,
mode: "popover",
mutationError: null,
openCreate: vi.fn(),
openEdit: vi.fn(),
openPopover: vi.fn(),
promptToDelete: null,
retry: vi.fn(),
reorderCapabilityAvailable: true,
reorderError: null,
reorderPrompts: vi.fn(async () => true),
saveDraft: vi.fn(async () => true),
searchQuery: "",
showReorderHandles: false,
selectPrompt: vi.fn(),
selectedPrompt: null,
setPopoverOpen: vi.fn(),
setSearchQuery: vi.fn(),
snapshot: {
enabled: true,
status: "ready",
prompts: [prompt],
error: null,
revision: 1,
pendingMutationIds: []
},
submitDelete: vi.fn(async () => true),
...patch
};
}
describe("AgentQuickPromptPopover", () => {
it("uses the shared composer trigger color and icon sizing", () => {
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={controller()} disabled={false} />
</TooltipProvider>
);
const trigger = screen.getByRole("button", {
name: "Choose a quick prompt"
});
expect(trigger).toHaveClass("agent-gui-node__composer-menu-trigger");
expect(trigger).toHaveClass("focus-visible:!outline-2");
expect(trigger.querySelector("svg")).toHaveClass("size-4");
});
it("keeps selection and direct icon management controls as sibling buttons", () => {
const subject = controller();
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={subject} disabled={false} />
</TooltipProvider>
);
const selection = screen.getByRole("button", { name: /^Review/u });
expect(selection.querySelector("button")).toBeNull();
const edit = screen.getByRole("button", { name: "Edit" });
const remove = screen.getByRole("button", { name: "Delete" });
expect(edit).toBeInTheDocument();
expect(remove).toBeInTheDocument();
fireEvent.pointerDown(edit, { button: 0 });
expect(subject.openEdit).toHaveBeenCalledOnce();
fireEvent.pointerDown(remove, { button: 0 });
expect(subject.deletePrompt).toHaveBeenCalledOnce();
});
it("shows reorder handles only in explicit sorting mode", () => {
const first = controller().filteredPrompts[0]!;
const second = {
...first,
id: "prompt-2",
title: "Plan",
content: "Plan the current change"
};
const subject = controller({
canReorder: true,
filteredPrompts: [first, second],
showReorderHandles: true,
snapshot: {
enabled: true,
status: "ready",
prompts: [first, second],
error: null,
revision: 1,
pendingMutationIds: []
}
});
const rendered = render(
<TooltipProvider>
<AgentQuickPromptPopover controller={subject} disabled={false} />
</TooltipProvider>
);
expect(screen.queryByRole("button", { name: "Reorder Review" })).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Reorder" }));
expect(
screen.getByRole("button", { name: "Reorder Review" })
).toBeEnabled();
expect(screen.queryByPlaceholderText("Search quick prompts")).toBeNull();
expect(screen.queryByRole("button", { name: "Edit" })).toBeNull();
expect(screen.queryByRole("button", { name: "Delete" })).toBeNull();
expect(
screen.queryByRole("button", {
name: /Summarize common prompts.*Use template/u
})
).toBeNull();
expect(
screen.queryByRole("button", {
name: "Recommended templates"
})
).toBeNull();
rendered.rerender(
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({
canReorder: false,
filteredPrompts: [first, second],
isInteractionLocked: true,
showReorderHandles: true,
snapshot: {
enabled: true,
status: "ready",
prompts: [first, second],
error: null,
revision: 2,
orderMutationPending: true,
pendingMutationIds: [first.id]
}
})}
disabled={false}
/>
</TooltipProvider>
);
expect(
screen.getByRole("button", { name: "Reorder Review" })
).toBeDisabled();
expect(screen.queryByRole("button", { name: /^Review/u })).toBeNull();
expect(screen.getByText("Review")).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Done" })).toBeDisabled();
});
it("enters and finishes sorting on primary pointer down and keyboard click", () => {
const first = controller().filteredPrompts[0]!;
const second = { ...first, id: "prompt-2", title: "Plan" };
render(
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({
canReorder: true,
filteredPrompts: [first, second],
showReorderHandles: true,
snapshot: {
enabled: true,
status: "ready",
prompts: [first, second],
error: null,
revision: 1,
pendingMutationIds: []
}
})}
disabled={false}
/>
</TooltipProvider>
);
const sort = screen.getByRole("button", { name: "Reorder" });
fireEvent.pointerDown(sort, { button: 0 });
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
const done = screen.getByRole("button", { name: "Done" });
fireEvent.pointerDown(done, { button: 0 });
expect(screen.getByRole("button", { name: "Reorder" })).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Search quick prompts")
).toBeInTheDocument();
fireEvent.click(screen.getByRole("button", { name: "Reorder" }));
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
expect(screen.queryByPlaceholderText("Search quick prompts")).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Done" }));
expect(screen.getByRole("button", { name: "Reorder" })).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Search quick prompts")
).toBeInTheDocument();
});
it("reports a Composer insertion failure without reporting a save failure", () => {
render(
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({ insertionError: true })}
disabled={false}
/>
</TooltipProvider>
);
expect(screen.getByRole("alert")).toHaveTextContent(
"Could not insert into the Composer. Try again from the quick prompt list."
);
});
it("returns to the prompt list when insertion fails from the template view", () => {
const rendered = render(
<TooltipProvider>
<AgentQuickPromptPopover controller={controller()} disabled={false} />
</TooltipProvider>
);
fireEvent.click(
screen.getByRole("button", { name: "Recommended templates" })
);
expect(
screen.getByRole("heading", { name: "Recommended templates", level: 2 })
).toBeInTheDocument();
rendered.rerender(
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({ insertionError: true })}
disabled={false}
/>
</TooltipProvider>
);
expect(
screen.getByRole("heading", { name: "Quick prompts", level: 2 })
).toBeInTheDocument();
expect(screen.getByRole("alert")).toHaveTextContent(
"Could not insert into the Composer. Try again from the quick prompt list."
);
expect(
screen.getByRole("button", { name: /^Review/u })
).toBeInTheDocument();
});
it("explains why reordering is unavailable with stable reason priority", () => {
const first = controller().filteredPrompts[0]!;
const second = { ...first, id: "prompt-2", title: "Plan" };
const rendered = render(
<TooltipProvider>
<AgentQuickPromptPopover controller={controller()} disabled={false} />
</TooltipProvider>
);
expect(
screen.getByLabelText(
"At least two quick prompts are required to adjust their order"
)
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Reorder" })).toBeDisabled();
rendered.rerender(
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({
filteredPrompts: [first, second],
reorderCapabilityAvailable: false,
showReorderHandles: false,
snapshot: {
enabled: true,
status: "ready",
prompts: [first, second],
error: null,
revision: 1,
pendingMutationIds: []
}
})}
disabled={false}
/>
</TooltipProvider>
);
expect(
screen.getByLabelText("This host does not support prompt reordering")
).toBeInTheDocument();
rendered.rerender(
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({
filteredPrompts: [],
snapshot: {
enabled: true,
status: "loading",
prompts: [],
error: null,
revision: 1,
pendingMutationIds: []
}
})}
disabled={false}
/>
</TooltipProvider>
);
expect(screen.getByLabelText(labels.loading)).toBeInTheDocument();
rendered.rerender(
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({
filteredPrompts: [],
snapshot: {
enabled: true,
status: "error",
prompts: [],
error: "offline",
revision: 1,
pendingMutationIds: []
}
})}
disabled={false}
/>
</TooltipProvider>
);
expect(screen.getByLabelText(labels.loadError)).toBeInTheDocument();
});
it("resets sorting mode when the controlled Popover closes externally", () => {
const first = controller().filteredPrompts[0]!;
const second = { ...first, id: "prompt-2", title: "Plan" };
const openController = controller({
canReorder: true,
filteredPrompts: [first, second],
showReorderHandles: true,
snapshot: {
enabled: true,
status: "ready",
prompts: [first, second],
error: null,
revision: 1,
pendingMutationIds: []
}
});
const rendered = render(
<TooltipProvider>
<AgentQuickPromptPopover controller={openController} disabled={false} />
</TooltipProvider>
);
fireEvent.click(screen.getByRole("button", { name: "Reorder" }));
expect(screen.getByRole("button", { name: "Done" })).toBeInTheDocument();
rendered.rerender(
<TooltipProvider>
<AgentQuickPromptPopover
controller={{ ...openController, isPopoverOpen: false }}
disabled={false}
/>
</TooltipProvider>
);
rendered.rerender(
<TooltipProvider>
<AgentQuickPromptPopover controller={openController} disabled={false} />
</TooltipProvider>
);
expect(screen.getByRole("button", { name: "Reorder" })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: "Done" })).toBeNull();
expect(screen.queryByRole("button", { name: "Reorder Review" })).toBeNull();
});
it("disables recommended-template actions while a shared mutation is pending", () => {
const subject = controller({
filteredPrompts: [],
isInteractionLocked: true,
snapshot: {
enabled: true,
status: "ready",
prompts: [],
error: null,
revision: 1,
pendingMutationIds: ["prompt-1"]
}
});
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={subject} disabled={false} />
</TooltipProvider>
);
const summaryTemplate = screen.getByRole("button", {
name: /Summarize common prompts.*Use template/u
});
const createTemplate = screen.getByRole("button", {
name: /Understand the situation.*Use template/u
});
expect(summaryTemplate).toBeDisabled();
expect(createTemplate).toBeDisabled();
fireEvent.pointerDown(summaryTemplate, { button: 0 });
fireEvent.click(summaryTemplate, { detail: 1 });
expect(subject.openCreate).not.toHaveBeenCalled();
});
it("selects on primary pointer down and still accepts a later keyboard click", () => {
const subject = controller();
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={subject} disabled={false} />
</TooltipProvider>
);
const prompt = screen.getByRole("button", { name: /^Review/u });
fireEvent.pointerDown(prompt, { button: 0 });
expect(subject.selectPrompt).toHaveBeenCalledOnce();
fireEvent.click(prompt);
expect(subject.selectPrompt).toHaveBeenCalledTimes(2);
});
it("keeps touch and pen actions cancellable until click", () => {
const subject = controller();
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={subject} disabled={false} />
</TooltipProvider>
);
const prompt = screen.getByRole("button", { name: /^Review/u });
fireEvent.pointerDown(prompt, { button: 0, pointerType: "touch" });
fireEvent.pointerCancel(prompt, { pointerType: "touch" });
expect(subject.selectPrompt).not.toHaveBeenCalled();
fireEvent.pointerDown(prompt, { button: 0, pointerType: "pen" });
expect(subject.selectPrompt).not.toHaveBeenCalled();
fireEvent.click(prompt, { detail: 1 });
expect(subject.selectPrompt).toHaveBeenCalledOnce();
});
it("keeps the common-prompts recommendation inside the template view", () => {
const subject = controller();
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={subject} disabled={false} />
</TooltipProvider>
);
expect(
screen.queryByRole("button", {
name: /Summarize common prompts.*Use template/u
})
).toBeNull();
fireEvent.pointerDown(
screen.getByRole("button", { name: "Recommended templates" }),
{ button: 0 }
);
const summaryTemplate = screen.getByRole("button", {
name: /Summarize common prompts.*Use template/u
});
fireEvent.pointerDown(summaryTemplate, { button: 0 });
fireEvent.click(summaryTemplate, { detail: 1 });
expect(subject.openCreate).toHaveBeenCalledWith(
{
title: "Summarize common prompts",
content: "Summarize my common prompts"
},
{
insertIntoComposerAfterSave: true,
usagePromptType: "recommended_template"
}
);
expect(subject.openCreate).toHaveBeenCalledOnce();
expect(subject.saveDraft).not.toHaveBeenCalled();
});
it("starts creation on primary pointer down so the Dialog survives Popover dismissal", () => {
const subject = controller();
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={subject} disabled={false} />
</TooltipProvider>
);
fireEvent.pointerDown(screen.getByRole("button", { name: "New prompt" }), {
button: 0
});
expect(subject.openCreate).toHaveBeenCalledOnce();
});
it("prefills the create editor for every recommended template", () => {
const subject = controller({
filteredPrompts: [],
snapshot: {
enabled: true,
status: "ready",
prompts: [],
error: null,
revision: 1,
pendingMutationIds: []
}
});
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={subject} disabled={false} />
</TooltipProvider>
);
const summaryTemplate = screen.getByRole("button", {
name: /Summarize common prompts.*Use template/u
});
fireEvent.click(summaryTemplate);
expect(subject.openCreate).toHaveBeenNthCalledWith(
1,
{
title: "Summarize common prompts",
content: "Summarize my common prompts"
},
{
insertIntoComposerAfterSave: true,
usagePromptType: "recommended_template"
}
);
const createTemplate = screen.getByRole("button", {
name: /Understand the situation.*Use template/u
});
fireEvent.click(createTemplate);
expect(subject.openCreate).toHaveBeenNthCalledWith(
2,
{
title: "Understand the situation",
content: "Summarize the situation"
},
{
insertIntoComposerAfterSave: true,
usagePromptType: "recommended_template"
}
);
expect(subject.openCreate).toHaveBeenCalledTimes(2);
});
it("lets a non-empty library reopen recommended templates in the same Popover", () => {
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={controller()} disabled={false} />
</TooltipProvider>
);
fireEvent.pointerDown(
screen.getByRole("button", {
name: "Recommended templates"
}),
{ button: 0 }
);
expect(
screen.getByRole("heading", { name: "Recommended templates", level: 2 })
).toBeInTheDocument();
expect(screen.queryByPlaceholderText("Search quick prompts")).toBeNull();
expect(
screen.getByRole("button", { name: "My prompts" })
).toBeInTheDocument();
fireEvent.pointerDown(screen.getByRole("button", { name: "My prompts" }), {
button: 0
});
expect(
screen.getByRole("heading", { name: "Quick prompts", level: 2 })
).toBeInTheDocument();
expect(
screen.getByPlaceholderText("Search quick prompts")
).toBeInTheDocument();
});
it("runs dialog cancellation and deletion on primary pointer down", () => {
const createSubject = controller({
isEditorOpen: true,
isPopoverOpen: false,
mode: "create"
});
const createRender = render(
<TooltipProvider>
<AgentQuickPromptPopover controller={createSubject} disabled={false} />
</TooltipProvider>
);
fireEvent.pointerDown(screen.getByRole("button", { name: labels.cancel }), {
button: 0
});
expect(createSubject.closeDialog).toHaveBeenCalledOnce();
createRender.unmount();
const deleteSubject = controller({
isPopoverOpen: false,
mode: "delete",
promptToDelete: controller().filteredPrompts[0]!
});
render(
<TooltipProvider>
<AgentQuickPromptPopover controller={deleteSubject} disabled={false} />
</TooltipProvider>
);
fireEvent.pointerDown(
screen.getByRole("button", { name: labels.deleteConfirm }),
{ button: 0 }
);
expect(deleteSubject.submitDelete).toHaveBeenCalledOnce();
});
it("preserves an edited draft when a conflict refreshes the same prompt", () => {
const prompt = controller().filteredPrompts[0]!;
const rendered = render(
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({
isEditorOpen: true,
isPopoverOpen: false,
mode: "edit",
selectedPrompt: prompt
})}
disabled={false}
/>
</TooltipProvider>
);
const content = screen.getByLabelText(labels.contentLabel);
fireEvent.change(content, { target: { value: "My unsaved draft" } });
rendered.rerender(
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({
isEditorOpen: true,
isPopoverOpen: false,
mode: "edit",
selectedPrompt: { ...prompt, version: 2, updatedAtUnixMs: 5 }
})}
disabled={false}
/>
</TooltipProvider>
);
expect(screen.getByLabelText(labels.contentLabel)).toHaveValue(
"My unsaved draft"
);
});
it("keeps Enter in the editor Dialog out of the Composer shortcut", () => {
const onComposerKeyDown = vi.fn();
render(
<div onKeyDown={onComposerKeyDown}>
<TooltipProvider>
<AgentQuickPromptPopover
controller={controller({
isEditorOpen: true,
isPopoverOpen: false,
mode: "create"
})}
disabled={false}
/>
</TooltipProvider>
</div>
);
fireEvent.keyDown(screen.getByLabelText(labels.titleLabel), {
key: "Enter"
});
expect(onComposerKeyDown).not.toHaveBeenCalled();
});
});