== User stories == US1: Контекст с дашборда/датасета → /agent с URL params US2: Guardrails card — env badge, 7 risk tones, countdown, permission_denied US3: Tools optimization — retry, timeout, summarise, RBAC + context affinity == Backend == - _context.py (NEW): UIContext validation (7 checks) - _tool_filter.py (NEW): RBAC + context affinity pipeline - _confirmation.py: build_confirmation_contract_v2, permission_denied_payload - tools.py: superset_list_databases, retry/summarise/timeout wrappers - app.py: _inject_uicontext, _inject_env_id_into_tools, database prefetch в runtime context - _persistence.py: prefetch_databases() - agent_superset_explore.py: GET /databases endpoint - _llm_async_http.py, _persistence.py: fix double /v1 в LLM URL (LM Studio Unexpected endpoint) == Frontend == - AgentChatModel.svelte.ts: 5 atoms, 3 actions, countdown, context - AgentChat.svelte: production banner, process steps, debug panel - ConfirmationCard.svelte: 7 risk tones, permission_denied, countdown - ToolCallCard.svelte: retrying/timeout/cancelled states - StreamProcessor.svelte.ts: tool_retry, timeout, permission_denied - TopNavbar: sparkles icon + Ассистент - sidebarNavigation: AI section - DashboardHeader, datasets/+page: contextual AI buttons - Icon: sparkles, brain, cpu icons - tailwind: assistant category colors - i18n: en/ru nav keys == Tests == - 159 backend agent tests (+16 US3: retry, timeout, summarise, contracts) - 2544 frontend tests (+11 model + component tests) - 15 JSON fixtures (10 API + 5 model) == Specs == - specs/035-agent-chat-context/: spec, UX, plan, tasks, research, data-model, contracts, quickstart, traceability, fixtures, checklists Closes #035
138 lines
4.9 KiB
TypeScript
138 lines
4.9 KiB
TypeScript
// #region Test.AgentChat.ToolCallCard.Ux [C:3] [TYPE Module] [SEMANTICS test,agent,tools,ux]
|
||
// @BRIEF L2 UX tests for ToolCallCard retrying/timeout/cancelled states — verifies visual indicators, action buttons, and callback wiring.
|
||
// @RELATION BINDS_TO -> [AgentChat.ToolCallCard]
|
||
// @TEST_EDGE: retrying_rendered -> status=retrying shows ⟳ spinner with retry count text
|
||
// @TEST_EDGE: read_timeout_rendered -> status=timeout (read tool) shows timeout message and retry button
|
||
// @TEST_EDGE: write_timeout_rendered -> status=timeout (write tool) shows Superset status message, NO retry button
|
||
// @TEST_EDGE: cancelled_rendered -> status=cancelled shows 🚫 icon and user-cancelled message
|
||
import { describe, it, expect, vi } from "vitest";
|
||
import { render, screen, fireEvent } from "@testing-library/svelte";
|
||
import ToolCallCard from "../ToolCallCard.svelte";
|
||
|
||
// #region Test.AgentChat.ToolCallCard.Ux.Retrying [C:2] [TYPE Test]
|
||
// @UX_STATE retrying — ⟳ spinner with retry count text.
|
||
describe("ToolCallCard — retrying state", () => {
|
||
it("shows spinner icon and retry count", () => {
|
||
const { container } = render(ToolCallCard, {
|
||
props: {
|
||
tool: "run_migration",
|
||
status: "retrying",
|
||
retryCount: 1,
|
||
maxRetries: 2,
|
||
},
|
||
});
|
||
// ⟳ spinner with animate-spin class
|
||
const spinner = container.querySelector("span.animate-spin");
|
||
expect(spinner).toBeTruthy();
|
||
expect(spinner?.textContent?.trim()).toBe("⟳");
|
||
// Retry count text
|
||
expect(container.textContent).toContain("Повторная попытка...");
|
||
expect(container.textContent).toMatch(/\(1\/2\)/);
|
||
});
|
||
|
||
it("shows tool name during retry", () => {
|
||
render(ToolCallCard, {
|
||
props: {
|
||
tool: "run_migration",
|
||
status: "retrying",
|
||
retryCount: 1,
|
||
maxRetries: 2,
|
||
},
|
||
});
|
||
expect(screen.getByText("run_migration")).toBeTruthy();
|
||
});
|
||
});
|
||
// #endregion Test.AgentChat.ToolCallCard.Ux.Retrying
|
||
|
||
// #region Test.AgentChat.ToolCallCard.Ux.ReadTimeout [C:2] [TYPE Test]
|
||
// @UX_STATE timeout (read tool) — ⏱️ icon, timeout message, retry button with onRetry callback.
|
||
describe("ToolCallCard — read timeout state", () => {
|
||
it("shows timeout icon and message", () => {
|
||
const { container } = render(ToolCallCard, {
|
||
props: {
|
||
tool: "search_dashboards",
|
||
status: "timeout",
|
||
timeoutSeconds: 30,
|
||
isWriteTool: false,
|
||
},
|
||
});
|
||
expect(container.textContent).toContain("⏱️");
|
||
expect(container.textContent).toContain("Превышено время ожидания");
|
||
expect(container.textContent).toContain("30с");
|
||
});
|
||
|
||
it("shows retry button", () => {
|
||
render(ToolCallCard, {
|
||
props: {
|
||
tool: "search_dashboards",
|
||
status: "timeout",
|
||
timeoutSeconds: 30,
|
||
isWriteTool: false,
|
||
},
|
||
});
|
||
expect(screen.getByRole("button", { name: /Повторить/ })).toBeTruthy();
|
||
});
|
||
|
||
it("fires onRetry callback when retry button is clicked", async () => {
|
||
const onRetry = vi.fn();
|
||
render(ToolCallCard, {
|
||
props: {
|
||
tool: "search_dashboards",
|
||
status: "timeout",
|
||
timeoutSeconds: 30,
|
||
isWriteTool: false,
|
||
onRetry,
|
||
},
|
||
});
|
||
const retryBtn = screen.getByRole("button", { name: /Повторить/ });
|
||
await fireEvent.click(retryBtn);
|
||
expect(onRetry).toHaveBeenCalledOnce();
|
||
});
|
||
});
|
||
// #endregion Test.AgentChat.ToolCallCard.Ux.ReadTimeout
|
||
|
||
// #region Test.AgentChat.ToolCallCard.Ux.WriteTimeout [C:2] [TYPE Test]
|
||
// @UX_STATE timeout (write tool) — Superset status message, NO retry button.
|
||
describe("ToolCallCard — write timeout state", () => {
|
||
it("shows Superset status message", () => {
|
||
const { container } = render(ToolCallCard, {
|
||
props: {
|
||
tool: "migrate_dashboard",
|
||
status: "timeout",
|
||
timeoutSeconds: 30,
|
||
isWriteTool: true,
|
||
},
|
||
});
|
||
expect(container.textContent).toContain("Проверьте статус операции в Superset");
|
||
});
|
||
|
||
it("does NOT show retry button", () => {
|
||
render(ToolCallCard, {
|
||
props: {
|
||
tool: "migrate_dashboard",
|
||
status: "timeout",
|
||
isWriteTool: true,
|
||
},
|
||
});
|
||
expect(screen.queryByRole("button", { name: /Повторить/ })).toBeNull();
|
||
});
|
||
});
|
||
// #endregion Test.AgentChat.ToolCallCard.Ux.WriteTimeout
|
||
|
||
// #region Test.AgentChat.ToolCallCard.Ux.Cancelled [C:2] [TYPE Test]
|
||
// @UX_STATE cancelled — 🚫 icon and user-cancelled message.
|
||
describe("ToolCallCard — cancelled state", () => {
|
||
it("shows cancelled icon and message", () => {
|
||
const { container } = render(ToolCallCard, {
|
||
props: {
|
||
tool: "search_dashboards",
|
||
status: "cancelled",
|
||
},
|
||
});
|
||
expect(container.textContent).toContain("🚫");
|
||
expect(container.textContent).toContain("Отменено пользователем");
|
||
});
|
||
});
|
||
// #endregion Test.AgentChat.ToolCallCard.Ux.Cancelled
|
||
// #endregion Test.AgentChat.ToolCallCard.Ux
|