Files
ss-tools/frontend/src/lib/components/assistant/__tests__/ToolCallCard.ux.test.ts
busya ff60865183 feat(agent-chat): 035-agent-chat-context — контекст, guardrails, tools, database discovery
== 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
2026-07-04 22:47:17 +03:00

138 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// #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