import { describe, expect, it, vi } from "vitest" import { render, screen, waitFor } from "@testing-library/react" import { QueryClient, QueryClientProvider } from "@tanstack/react-query" import type { AktivitaetResp } from "@/lib/api" // Die Query wird ersetzt, nicht das Netz: Der Test prüft die ANSICHT, nicht den Abruf. const useWerkzeugverlauf = vi.hoisted(() => vi.fn()) vi.mock("@/lib/queries", () => ({ useWerkzeugverlauf })) const { Werkzeugverlauf } = await import("./Werkzeugverlauf") function zeigen(data: AktivitaetResp | undefined, isLoading = false) { useWerkzeugverlauf.mockReturnValue({ data, isLoading }) const qc = new QueryClient({ defaultOptions: { queries: { retry: false } } }) return render( , ) } const ANTWORT: AktivitaetResp = { verfuegbar: true, pfad: "~/.hermes/logs/agent.log", rufe: [ { zeit: "2026-08-28T07:01:20", lauf: "cron_a", werkzeug: "web_extract", dauer_s: 0.17, zeichen: null, ok: false, fehler: "DuckDuckGo (ddgs) is a search-only backend." }, { zeit: "2026-08-28T07:02:36", lauf: null, werkzeug: "web_search", dauer_s: 2.61, zeichen: 2944, ok: true, fehler: null }, { zeit: "2026-08-28T07:03:18", lauf: "cron_a", werkzeug: "terminal", dauer_s: 1.4, zeichen: 53, ok: true, fehler: null }, ], werkzeuge: [ { werkzeug: "web_extract", rufe: 2, fehler: 2, dauer_schnitt_s: 0.17, letzter_fehler: "DuckDuckGo (ddgs) is a search-only backend." }, { werkzeug: "web_search", rufe: 20, fehler: 0, dauer_schnitt_s: 1.77, letzter_fehler: null }, ], laeufe: [{ lauf: "cron_a", von: "2026-08-28T07:01:05", bis: "2026-08-28T07:03:18", rufe: 3, fehler: 1 }], } /** Die Bilanz-Karte eines Werkzeugs — der Name steht auch in der Zeitleiste, deshalb * wird hier gezielt die Karte gesucht und nicht irgendein Vorkommen des Namens. */ function bilanzKarte(werkzeug: string): HTMLElement { const treffer = screen.getAllByText(werkzeug) .map((el) => el.closest("div.mc-card-sm")) .filter((el): el is HTMLElement => el instanceof HTMLElement && el.textContent!.includes("Rufe")) expect(treffer.length, `genau eine Bilanz-Karte fuer ${werkzeug}`).toBe(1) return treffer[0] } describe("Werkzeugverlauf", () => { // Der eigentliche Zweck der Ansicht: Ein Werkzeug, das IMMER scheitert, verschwindet // in einer Ereignisliste. Genau so lief `web_extract` vier Tage lang jeden Morgen // ins Leere, ohne dass es jemandem auffiel. it("markiert ein durchgehend scheiterndes Werkzeug als „immer rot“", () => { zeigen(ANTWORT) const karte = bilanzKarte("web_extract") expect(karte.textContent).toMatch(/immer rot/i) expect(karte.textContent).toMatch(/2 Fehler/) }) it("markiert ein fehlerfreies Werkzeug NICHT", () => { zeigen(ANTWORT) const karte = bilanzKarte("web_search") expect(karte.textContent).not.toMatch(/immer rot/i) expect(karte.textContent).toMatch(/0 Fehler/) // Und die Markierung gibt es im ganzen Abschnitt genau einmal. expect(screen.getAllByText(/immer rot/i)).toHaveLength(1) }) it("zeigt den echten Grund des Fehlers, nicht nur „Fehler“", () => { zeigen(ANTWORT) expect(bilanzKarte("web_extract").textContent).toMatch(/search-only backend/) }) it("nennt Rufe und Fehler in der Abschnitts-Überschrift", () => { const { container } = zeigen(ANTWORT) // Die Überschrift setzt sich aus mehreren Textknoten zusammen (der Fehler-Teil // erscheint nur bedingt), deshalb gegen den zusammengesetzten Text prüfen. const kopf = container.querySelector("section")!.textContent! expect(kopf).toMatch(/3 Rufe/) // 3 Einträge in ANTWORT.rufe expect(kopf).toMatch(/2 Fehler/) // Summe aus der Werkzeug-Bilanz }) it("gruppiert nach Lauf und zeigt dessen Zeitspanne", () => { zeigen(ANTWORT) const zeile = screen.getByText("cron_a").closest("div")! expect(zeile.textContent).toMatch(/07:01:05–07:03:18/) expect(zeile.textContent).toMatch(/1 Fehler/) }) // Auf einem Entwicklungsrechner ohne Hermes gibt es das Log nicht. Dann still // weglassen — eine leere Karte waere eine Zusage ohne Inhalt. it("verschwindet ganz, wenn es kein Hermes-Log gibt", () => { const { container } = zeigen({ verfuegbar: false, pfad: "", rufe: [], werkzeuge: [], laeufe: [] }) expect(container).toBeEmptyDOMElement() }) it("sagt beim Laden, was es tut", async () => { zeigen(undefined, true) await waitFor(() => expect(screen.getByText(/Lese Hermes/)).toBeInTheDocument()) }) })