Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion web-ui/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,8 @@
"bulkDeleteFailed": "{count, plural, one {# deck} other {# decks}} could not be deleted.",
"bulkDeleteDescriptionLocal": "The selected deck folders will be deleted immediately. This action cannot be undone.",
"errorUpdateVisibility": "Failed to update visibility.",
"errorDeleteDeck": "Failed to delete deck."
"errorDeleteDeck": "Failed to delete deck.",
"errorLoadDecks": "Failed to load decks."
},
"mcp": {
"connected": "Connected: {names}",
Expand Down
3 changes: 2 additions & 1 deletion web-ui/messages/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,8 @@
"bulkDeleteFailed": "{count, plural, other {# 件のデッキを削除できませんでした。}}",
"bulkDeleteDescriptionLocal": "選択したデッキのフォルダを即時に削除します。この操作は元に戻せません。",
"errorUpdateVisibility": "公開範囲を更新できませんでした",
"errorDeleteDeck": "デッキを削除できませんでした"
"errorDeleteDeck": "デッキを削除できませんでした",
"errorLoadDecks": "デッキを読み込めませんでした"
},
"mcp": {
"connected": "接続済み: {names}",
Expand Down
72 changes: 72 additions & 0 deletions web-ui/src/components/Settings.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
// SPDX-License-Identifier: MIT-0
/** Settings (local): agent selection toasts are translated and a failed save rolls back. */

import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"
import { cleanup, fireEvent, screen, waitFor } from "@testing-library/react"
import { toast } from "sonner"
import { renderWithIntl } from "@/test/renderWithIntl"

vi.mock("@/lib/mode", () => ({ IS_LOCAL: true }))
vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }))
vi.mock("@/services/localService", () => ({
getAgentDefinitions: vi.fn(),
listLocalModels: vi.fn(),
updateAgentSelection: vi.fn(),
}))

import { getAgentDefinitions, listLocalModels, updateAgentSelection } from "@/services/localService"
import { Settings } from "./Settings"

// jsdom has no scrollIntoView; cmdk and ModelPicker call it.
beforeAll(() => {
Element.prototype.scrollIntoView = vi.fn()
})

const AGENTS = [
{ fileName: "a.json", name: "Agent A", description: "" },
{ fileName: "b.json", name: "Agent B", description: "" },
]

async function pickOrchestrator(locale: "en" | "ja") {
renderWithIntl(<Settings open onOpenChange={() => {}} />, { locale })
const trigger = await screen.findByLabelText("Select orchestrator agent")
await waitFor(() => expect(trigger.textContent).toContain("Agent A"))
fireEvent.click(trigger)
fireEvent.click(screen.getByRole("option", { name: /Agent B/ }))
return trigger
}

describe("Settings (local) — agent selection", () => {
beforeEach(() => {
vi.mocked(getAgentDefinitions).mockResolvedValue({
agents: AGENTS,
selection: { orchestrator: "a.json", composer: "a.json", style: "a.json", translate: "a.json" },
})
vi.mocked(listLocalModels).mockResolvedValue([])
})

afterEach(() => {
cleanup()
vi.clearAllMocks()
})

it("shows the translated toast after saving", async () => {
vi.mocked(updateAgentSelection).mockResolvedValue(undefined)
const trigger = await pickOrchestrator("ja")
await waitFor(() => expect(toast.success).toHaveBeenCalledWith(
"orchestrator エージェントを更新しました",
{ description: "Agent B を使用します。次のチャットから反映され、現在のチャットには影響しません。" },
))
expect(updateAgentSelection).toHaveBeenCalledWith({ orchestrator: "b.json" })
expect(trigger.textContent).toContain("Agent B")
})

it("rolls the selection back and shows the translated error when the save fails", async () => {
vi.mocked(updateAgentSelection).mockRejectedValue(new Error("HTTP 500"))
const trigger = await pickOrchestrator("ja")
await waitFor(() => expect(toast.error).toHaveBeenCalledWith("orchestrator エージェントを更新できませんでした"))
expect(toast.success).not.toHaveBeenCalled()
await waitFor(() => expect(trigger.textContent).toContain("Agent A"))
})
})
13 changes: 12 additions & 1 deletion web-ui/src/hooks/useDeckList.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT-0

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
import { act, renderHook } from "@testing-library/react"
import { act, renderHook, waitFor } from "@testing-library/react"
import { toast } from "sonner"
import { intlWrapper } from "@/test/renderWithIntl"
import en from "../../messages/en.json"
Expand Down Expand Up @@ -109,4 +109,15 @@ describe("useDeckList — translated action errors", () => {
await act(async () => { await result.current.handleToggleVisibility("d1", "public") })
expect(toast.error).toHaveBeenCalledWith(expected)
})

it.each([
["en", en.decksPage.errorLoadDecks],
["ja", "デッキを読み込めませんでした"],
] as const)("shows the %s banner instead of the raw error when the list cannot load", async (locale, expected) => {
api.listDecks.mockRejectedValueOnce(new Error("Failed to list decks: 500"))
vi.spyOn(console, "error").mockImplementation(() => {})
const { result } = renderHook(() => useDeckList("token", true, null), { wrapper: intlWrapper(locale) })
await waitFor(() => expect(result.current.loading).toBe(false))
expect(result.current.error).toBe(expected)
})
})
8 changes: 6 additions & 2 deletions web-ui/src/hooks/useDeckList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ export function useDeckList(
const [favoriteIds, setFavoriteIds] = useState<Set<string>>(new Set())
const [activeListTab, setActiveListTab] = useState<"mine" | "favorites" | "public">("mine")
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
/** The first list load failed; translated at render (the raw error goes to the console). */
const [loadFailed, setLoadFailed] = useState(false)
const [deleteTarget, setDeleteTarget] = useState<DeckSummary | null>(null)
const [bulkDeleteTargets, setBulkDeleteTargets] = useState<DeckSummary[] | null>(null)
const [bulkProgress, setBulkProgress] = useState<{ done: number; total: number } | null>(null)
Expand Down Expand Up @@ -94,7 +95,8 @@ export function useDeckList(
setFavoriteDecks(favDecks)
} catch (err) {
if (!initialLoadDone.current) {
setError(err instanceof Error ? err.message : "Failed to load decks")
console.error("Failed to load decks", err)
setLoadFailed(true)
}
} finally {
initialLoadDone.current = true
Expand Down Expand Up @@ -263,6 +265,8 @@ export function useDeckList(
: activeListTab === "favorites" ? favoriteDecks
: publicDecks

const error = loadFailed ? t("errorLoadDecks") : null

return {
decks, tabDecks, favoriteIds, activeListTab, setActiveListTab,
loading, error, deleteTarget, setDeleteTarget,
Expand Down
Loading