mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-30 03:00:41 -04:00
Compare commits
6 Commits
master
...
fix/small-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b6b605f936 | ||
|
|
29215e9523 | ||
|
|
6456e0f317 | ||
|
|
12fae3132a | ||
|
|
1a38974f19 | ||
|
|
9c5d1e1019 |
@@ -246,7 +246,7 @@ export function ChatSidebar() {
|
||||
</SidebarHeader>
|
||||
)}
|
||||
{!isCollapsed && (
|
||||
<SidebarHeader className="shrink-0 px-4 pb-4 pt-4 shadow-[0_4px_6px_-1px_rgba(0,0,0,0.05)]">
|
||||
<SidebarHeader className="shrink-0 px-4 pb-3 pt-3 shadow-[0_4px_6px_-1px_rgba(0,0,0,0.05)]">
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
|
||||
@@ -16,7 +16,7 @@ export function EditNameDialog({ currentName }: Props) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [name, setName] = useState(currentName);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const { supabase, refreshSession } = useSupabase();
|
||||
const { refreshSession } = useSupabase();
|
||||
const { toast } = useToast();
|
||||
|
||||
function handleOpenChange(open: boolean) {
|
||||
@@ -26,29 +26,31 @@ export function EditNameDialog({ currentName }: Props) {
|
||||
|
||||
async function handleSave() {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed || !supabase) return;
|
||||
if (!trimmed) return;
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const { error } = await supabase.auth.updateUser({
|
||||
data: { full_name: trimmed },
|
||||
const res = await fetch("/api/auth/user", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ full_name: trimmed }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
if (!res.ok) {
|
||||
const body = await res.json();
|
||||
toast({
|
||||
title: "Failed to update name",
|
||||
description: error.message,
|
||||
description: body.error ?? "Unknown error",
|
||||
variant: "destructive",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await refreshSession();
|
||||
} catch (e) {
|
||||
const session = await refreshSession();
|
||||
if (session?.error) {
|
||||
toast({
|
||||
title: "Name saved, but session refresh failed",
|
||||
description: e instanceof Error ? e.message : "Please reload.",
|
||||
description: session.error,
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsOpen(false);
|
||||
|
||||
@@ -5,31 +5,37 @@ import {
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@/tests/integrations/test-utils";
|
||||
import { server } from "@/mocks/mock-server";
|
||||
import { http, HttpResponse } from "msw";
|
||||
import { EditNameDialog } from "../EditNameDialog";
|
||||
|
||||
const mockToast = vi.hoisted(() => vi.fn());
|
||||
const mockUseSupabase = vi.hoisted(() => vi.fn());
|
||||
const mockRefreshSession = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("@/components/molecules/Toast/use-toast", () => ({
|
||||
useToast: () => ({ toast: mockToast }),
|
||||
}));
|
||||
|
||||
vi.mock("@/lib/supabase/hooks/useSupabase", () => ({
|
||||
useSupabase: mockUseSupabase,
|
||||
useSupabase: () => ({
|
||||
refreshSession: mockRefreshSession,
|
||||
}),
|
||||
}));
|
||||
|
||||
function setup({
|
||||
updateUser = vi.fn().mockResolvedValue({ error: null }),
|
||||
refreshSession = vi.fn().mockResolvedValue(undefined),
|
||||
}: {
|
||||
updateUser?: ReturnType<typeof vi.fn>;
|
||||
refreshSession?: ReturnType<typeof vi.fn>;
|
||||
} = {}) {
|
||||
mockUseSupabase.mockReturnValue({
|
||||
supabase: { auth: { updateUser } },
|
||||
refreshSession,
|
||||
});
|
||||
return { updateUser, refreshSession };
|
||||
function mockUpdateNameSuccess() {
|
||||
server.use(
|
||||
http.put("/api/auth/user", () => {
|
||||
return HttpResponse.json({ user: { id: "u1" } });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
function mockUpdateNameError(message = "Network error") {
|
||||
server.use(
|
||||
http.put("/api/auth/user", () => {
|
||||
return HttpResponse.json({ error: message }, { status: 400 });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function openDialogAndGetInput() {
|
||||
@@ -49,19 +55,20 @@ function getSaveButton() {
|
||||
describe("EditNameDialog", () => {
|
||||
beforeEach(() => {
|
||||
mockToast.mockReset();
|
||||
mockUseSupabase.mockReset();
|
||||
mockRefreshSession.mockReset();
|
||||
mockRefreshSession.mockResolvedValue({ user: { id: "u1" } });
|
||||
});
|
||||
|
||||
test("opens dialog with current name prefilled", async () => {
|
||||
setup();
|
||||
mockUpdateNameSuccess();
|
||||
render(<EditNameDialog currentName="Alice" />);
|
||||
|
||||
const input = await openDialogAndGetInput();
|
||||
expect(input.value).toBe("Alice");
|
||||
});
|
||||
|
||||
test("saves name successfully and closes dialog", async () => {
|
||||
const { updateUser, refreshSession } = setup();
|
||||
test("saves name via API route and closes dialog", async () => {
|
||||
mockUpdateNameSuccess();
|
||||
render(<EditNameDialog currentName="Alice" />);
|
||||
|
||||
const input = await openDialogAndGetInput();
|
||||
@@ -69,21 +76,13 @@ describe("EditNameDialog", () => {
|
||||
fireEvent.click(getSaveButton());
|
||||
|
||||
await waitFor(() => {
|
||||
expect(updateUser).toHaveBeenCalledWith({ data: { full_name: "Bob" } });
|
||||
});
|
||||
expect(refreshSession).toHaveBeenCalled();
|
||||
await waitFor(() => {
|
||||
expect(mockToast).toHaveBeenCalledWith({ title: "Name updated" });
|
||||
expect(mockRefreshSession).toHaveBeenCalled();
|
||||
});
|
||||
expect(mockToast).toHaveBeenCalledWith({ title: "Name updated" });
|
||||
});
|
||||
|
||||
test("shows error toast when updateUser fails and keeps dialog open", async () => {
|
||||
const updateUser = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ error: { message: "Network error" } });
|
||||
const refreshSession = vi.fn();
|
||||
setup({ updateUser, refreshSession });
|
||||
|
||||
test("shows error toast when API returns error", async () => {
|
||||
mockUpdateNameError("Network error");
|
||||
render(<EditNameDialog currentName="Alice" />);
|
||||
|
||||
const input = await openDialogAndGetInput();
|
||||
@@ -99,15 +98,12 @@ describe("EditNameDialog", () => {
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(refreshSession).not.toHaveBeenCalled();
|
||||
expect(mockRefreshSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test("closes dialog and toasts failure when refreshSession throws", async () => {
|
||||
const updateUser = vi.fn().mockResolvedValue({ error: null });
|
||||
const refreshSession = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error("refresh failed"));
|
||||
setup({ updateUser, refreshSession });
|
||||
test("shows warning toast when refreshSession returns an error", async () => {
|
||||
mockUpdateNameSuccess();
|
||||
mockRefreshSession.mockResolvedValue({ error: "refresh failed" });
|
||||
|
||||
render(<EditNameDialog currentName="Alice" />);
|
||||
|
||||
@@ -119,6 +115,7 @@ describe("EditNameDialog", () => {
|
||||
expect(mockToast).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: "Name saved, but session refresh failed",
|
||||
description: "refresh failed",
|
||||
variant: "destructive",
|
||||
}),
|
||||
);
|
||||
@@ -126,8 +123,8 @@ describe("EditNameDialog", () => {
|
||||
expect(mockToast).not.toHaveBeenCalledWith({ title: "Name updated" });
|
||||
});
|
||||
|
||||
test("disables Save button while empty input", async () => {
|
||||
setup();
|
||||
test("disables Save button while input is empty", async () => {
|
||||
mockUpdateNameSuccess();
|
||||
render(<EditNameDialog currentName="Alice" />);
|
||||
|
||||
const input = await openDialogAndGetInput();
|
||||
|
||||
@@ -34,7 +34,7 @@ export function PulseChips({ chips, onChipClick }: Props) {
|
||||
View all <ArrowRightIcon size={12} />
|
||||
</NextLink>
|
||||
</div>
|
||||
<div className="flex gap-2 overflow-x-auto">
|
||||
<div className="flex gap-2 overflow-x-auto pb-1 scrollbar-thin scrollbar-track-transparent scrollbar-thumb-zinc-300">
|
||||
{chips.map((chip) => (
|
||||
<PulseChip key={chip.id} chip={chip} onAsk={onChipClick} />
|
||||
))}
|
||||
@@ -56,7 +56,7 @@ function PulseChip({ chip, onAsk }: ChipProps) {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.chip} relative flex shrink-0 flex-col items-start gap-2 rounded-medium border border-zinc-100 bg-white px-3 py-2`}
|
||||
className={`${styles.chip} relative flex w-[15rem] shrink-0 flex-col items-start gap-2 rounded-medium border border-zinc-100 bg-white px-3 py-2`}
|
||||
>
|
||||
<div className={`${styles.chipContent} w-full text-left`}>
|
||||
{chip.priority === "success" ? (
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { render, screen, fireEvent } from "@/tests/integrations/test-utils";
|
||||
import { PulseChips } from "../PulseChips";
|
||||
import type { PulseChipData } from "../types";
|
||||
|
||||
function makeChip(overrides: Partial<PulseChipData> = {}): PulseChipData {
|
||||
return {
|
||||
id: "chip-1",
|
||||
agentID: "agent-1",
|
||||
name: "Test Agent",
|
||||
status: "running",
|
||||
priority: "running",
|
||||
shortMessage: "Doing work…",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("PulseChips", () => {
|
||||
test("renders nothing when chips array is empty", () => {
|
||||
const { container } = render(<PulseChips chips={[]} />);
|
||||
expect(container.innerHTML).toBe("");
|
||||
});
|
||||
|
||||
test("renders chip names and messages", () => {
|
||||
const chips = [
|
||||
makeChip({ id: "1", name: "Alpha Bot", shortMessage: "Running task A" }),
|
||||
makeChip({ id: "2", name: "Beta Bot", shortMessage: "Running task B" }),
|
||||
];
|
||||
|
||||
render(<PulseChips chips={chips} />);
|
||||
|
||||
expect(screen.getByText("Alpha Bot")).toBeDefined();
|
||||
expect(screen.getByText("Running task A")).toBeDefined();
|
||||
expect(screen.getByText("Beta Bot")).toBeDefined();
|
||||
expect(screen.getByText("Running task B")).toBeDefined();
|
||||
});
|
||||
|
||||
test("renders section heading and View all link", () => {
|
||||
render(<PulseChips chips={[makeChip()]} />);
|
||||
|
||||
expect(screen.getByText("What's happening with your agents")).toBeDefined();
|
||||
expect(screen.getByText("View all")).toBeDefined();
|
||||
});
|
||||
|
||||
test("shows Completed badge for success priority chips", () => {
|
||||
render(
|
||||
<PulseChips
|
||||
chips={[makeChip({ priority: "success", status: "idle" })]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText("Completed")).toBeDefined();
|
||||
});
|
||||
|
||||
test("calls onChipClick with generated prompt when Ask is clicked", () => {
|
||||
const onChipClick = vi.fn();
|
||||
render(
|
||||
<PulseChips
|
||||
chips={[
|
||||
makeChip({
|
||||
name: "Error Agent",
|
||||
status: "error",
|
||||
priority: "error",
|
||||
}),
|
||||
]}
|
||||
onChipClick={onChipClick}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Ask"));
|
||||
|
||||
expect(onChipClick).toHaveBeenCalledWith(
|
||||
"What happened with Error Agent? It has an error — can you check?",
|
||||
);
|
||||
});
|
||||
|
||||
test("generates success prompt for completed chips", () => {
|
||||
const onChipClick = vi.fn();
|
||||
render(
|
||||
<PulseChips
|
||||
chips={[
|
||||
makeChip({
|
||||
name: "Done Agent",
|
||||
priority: "success",
|
||||
status: "idle",
|
||||
}),
|
||||
]}
|
||||
onChipClick={onChipClick}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Ask"));
|
||||
|
||||
expect(onChipClick).toHaveBeenCalledWith(
|
||||
"Done Agent just finished a run — can you summarize what it did?",
|
||||
);
|
||||
});
|
||||
|
||||
test("renders See link pointing to agent detail page", () => {
|
||||
render(<PulseChips chips={[makeChip({ agentID: "agent-xyz" })]} />);
|
||||
|
||||
const seeLink = screen.getByText("See").closest("a");
|
||||
expect(seeLink?.getAttribute("href")).toBe("/library/agents/agent-xyz");
|
||||
});
|
||||
});
|
||||
@@ -5,13 +5,53 @@ import { useSitrepItems } from "@/app/(platform)/library/components/SitrepItem/u
|
||||
import type { PulseChipData } from "./types";
|
||||
import { useMemo } from "react";
|
||||
|
||||
// TODO: remove QA fakes before merging
|
||||
const QA_FAKES: PulseChipData[] = [
|
||||
{
|
||||
id: "qa-1",
|
||||
agentID: "fake-1",
|
||||
name: "SEO Blog Writer with Advanced Keyword Research and Content Optimization",
|
||||
status: "running",
|
||||
priority: "running",
|
||||
shortMessage:
|
||||
"Writing a comprehensive long-form article on the latest AI trends in enterprise software development and deployment",
|
||||
},
|
||||
{
|
||||
id: "qa-2",
|
||||
agentID: "fake-2",
|
||||
name: "Multi-Cloud Data Pipeline Monitor and Alerting System",
|
||||
status: "error",
|
||||
priority: "error",
|
||||
shortMessage:
|
||||
"Connection to the primary data warehouse timed out after 30 retries — fallback region also unreachable",
|
||||
},
|
||||
{
|
||||
id: "qa-3",
|
||||
agentID: "fake-3",
|
||||
name: "Social Media Cross-Platform Scheduler and Analytics Dashboard",
|
||||
status: "idle",
|
||||
priority: "success",
|
||||
shortMessage:
|
||||
"All 12 scheduled posts across Twitter, LinkedIn, and Instagram were published successfully with engagement tracking enabled",
|
||||
},
|
||||
{
|
||||
id: "qa-4",
|
||||
agentID: "fake-4",
|
||||
name: "Customer Support Triage and Automatic Escalation Handler",
|
||||
status: "running",
|
||||
priority: "stale",
|
||||
shortMessage:
|
||||
"3 high-priority tickets awaiting classification — SLA breach warning for 2 enterprise accounts pending review",
|
||||
},
|
||||
];
|
||||
|
||||
export function usePulseChips(): PulseChipData[] {
|
||||
const { agents } = useLibraryAgents();
|
||||
|
||||
const sitrepItems = useSitrepItems(agents, 5);
|
||||
|
||||
return useMemo(() => {
|
||||
return sitrepItems.map((item) => ({
|
||||
const real = sitrepItems.map((item) => ({
|
||||
id: item.id,
|
||||
agentID: item.agentID,
|
||||
name: item.agentName,
|
||||
@@ -19,5 +59,6 @@ export function usePulseChips(): PulseChipData[] {
|
||||
priority: item.priority,
|
||||
shortMessage: item.message,
|
||||
}));
|
||||
return [...real, ...QA_FAKES];
|
||||
}, [sitrepItems]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Text } from "@/components/atoms/Text/Text";
|
||||
import { OverflowText } from "@/components/atoms/OverflowText/OverflowText";
|
||||
import { Emoji } from "@/components/atoms/Emoji/Emoji";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { FleetSummary, AgentStatusFilter } from "../../types";
|
||||
@@ -78,17 +79,19 @@ export function StatsGrid({ summary, activeTab, onTabChange }: Props) {
|
||||
type="button"
|
||||
onClick={() => onTabChange(tile.filter)}
|
||||
className={cn(
|
||||
"flex flex-col gap-1 rounded-medium border p-3 text-left shadow-md transition-all hover:shadow-lg",
|
||||
"flex min-w-0 flex-col gap-1 rounded-medium border p-3 text-left shadow-md transition-all hover:shadow-lg",
|
||||
isActive
|
||||
? "border-zinc-900 bg-zinc-50"
|
||||
: "border-zinc-100 bg-white",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-1.5">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<Emoji text={tile.emoji} size={18} />
|
||||
<Text variant="body" className="text-zinc-800">
|
||||
{tile.label}
|
||||
</Text>
|
||||
<OverflowText
|
||||
value={tile.label}
|
||||
variant="body"
|
||||
className="text-zinc-800"
|
||||
/>
|
||||
</div>
|
||||
<Text variant="h4">{value}</Text>
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { Select } from "@/components/atoms/Select/Select";
|
||||
import type { SelectOption } from "@/components/atoms/Select/Select";
|
||||
import { Select } from "@/components/atoms/Select/Select";
|
||||
import { FunnelIcon } from "@phosphor-icons/react";
|
||||
import type { AgentStatusFilter, FleetSummary } from "../../types";
|
||||
|
||||
@@ -32,7 +32,9 @@ export function AgentFilterMenu({ value, onChange, summary }: Props) {
|
||||
|
||||
return (
|
||||
<div className="flex items-center" data-testid="agent-filter-dropdown">
|
||||
<span className="hidden whitespace-nowrap text-sm sm:inline">filter</span>
|
||||
<span className="hidden whitespace-nowrap text-sm text-zinc-500 sm:inline">
|
||||
filter
|
||||
</span>
|
||||
<FunnelIcon className="ml-1 h-4 w-4 sm:hidden" />
|
||||
<Select
|
||||
id="agent-status-filter"
|
||||
@@ -42,7 +44,7 @@ export function AgentFilterMenu({ value, onChange, summary }: Props) {
|
||||
onValueChange={handleChange}
|
||||
options={options}
|
||||
size="small"
|
||||
className="ml-1 w-fit border-none px-0 text-sm underline underline-offset-4 shadow-none"
|
||||
className="ml-1 w-fit border-none !bg-transparent text-sm underline underline-offset-4 shadow-none"
|
||||
wrapperClassName="mb-0"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -19,11 +19,11 @@ export function LibrarySortMenu({ setLibrarySort }: Props) {
|
||||
const { handleSortChange } = useLibrarySortMenu({ setLibrarySort });
|
||||
return (
|
||||
<div className="flex items-center" data-testid="sort-by-dropdown">
|
||||
<span className="hidden whitespace-nowrap text-sm sm:inline">
|
||||
<span className="hidden whitespace-nowrap text-sm text-zinc-500 sm:inline">
|
||||
sort by
|
||||
</span>
|
||||
<Select onValueChange={handleSortChange}>
|
||||
<SelectTrigger className="ml-1 w-fit space-x-1 border-none px-0 text-sm underline underline-offset-4 shadow-none">
|
||||
<SelectTrigger className="!m-0 ml-1 w-fit space-x-1 border-none !bg-transparent px-[1rem] text-sm underline underline-offset-4 !shadow-none !ring-offset-transparent">
|
||||
<ArrowDownNarrowWideIcon className="h-4 w-4 sm:hidden" />
|
||||
<SelectValue placeholder="Last Modified" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -15,15 +15,35 @@ export async function GET() {
|
||||
export async function PUT(request: Request) {
|
||||
try {
|
||||
const supabase = await getServerSupabase();
|
||||
const { email } = await request.json();
|
||||
|
||||
if (!email) {
|
||||
return NextResponse.json({ error: "Email is required" }, { status: 400 });
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { data, error } = await supabase.auth.updateUser({
|
||||
email,
|
||||
});
|
||||
const { email: rawEmail, full_name: rawFullName } = body as {
|
||||
email?: unknown;
|
||||
full_name?: unknown;
|
||||
};
|
||||
|
||||
const email = typeof rawEmail === "string" ? rawEmail.trim() : undefined;
|
||||
const fullName =
|
||||
typeof rawFullName === "string" ? rawFullName.trim() : undefined;
|
||||
|
||||
if (!email && !fullName) {
|
||||
return NextResponse.json(
|
||||
{ error: "Email or full_name is required" },
|
||||
{ status: 400 },
|
||||
);
|
||||
}
|
||||
|
||||
const updatePayload: Parameters<typeof supabase.auth.updateUser>[0] = {};
|
||||
if (email) updatePayload.email = email;
|
||||
if (fullName) updatePayload.data = { full_name: fullName };
|
||||
|
||||
const { data, error } = await supabase.auth.updateUser(updatePayload);
|
||||
|
||||
if (error) {
|
||||
return NextResponse.json({ error: error.message }, { status: 400 });
|
||||
@@ -32,7 +52,7 @@ export async function PUT(request: Request) {
|
||||
return NextResponse.json(data);
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to update user email" },
|
||||
{ error: "Failed to update user" },
|
||||
{ status: 500 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ export function Navbar() {
|
||||
<PreviewBanner branchName={previewBranchName} />
|
||||
) : null}
|
||||
<nav
|
||||
className="inline-flex w-full items-center bg-[#FAFAFA]/80 p-3 backdrop-blur-xl"
|
||||
className="inline-flex w-full items-center border-b border-[#f1f1f1] bg-[#FAFAFA]/80 p-3 backdrop-blur-xl"
|
||||
style={{ height: NAVBAR_HEIGHT_PX }}
|
||||
>
|
||||
{/* Left section */}
|
||||
|
||||
Reference in New Issue
Block a user