Files
mission-control-v2/frontend/src/features/agent/Werkzeugverlauf.test.tsx
T
HitonabiandClaude Opus 5 a494d320d7 feat(agent): P6 — Werkzeug-Verlauf, und der Blocker war nur eine halbe Wand
Der Blueprint sah eine "Live Agent Matrix" mit Denkstrom vor (§4.4). In P4 hatte ich
sie als blockiert gemeldet: Lucys Denkschritte entstehen im Hermes-Prozess, und
AGENTS.md verbietet es, dessen Quellcode zu patchen.

Beim Nachsehen zeigte sich, dass die HAELFTE davon offen daliegt.
`~/.hermes/logs/agent.log` protokolliert JEDEN Werkzeug-Ruf:

  INFO [cron_195e…] agent.tool_executor: tool terminal completed (1.40s, 53 chars)
  WARNING [cron_195e…] agent.tool_executor: Tool web_extract returned error (0.17s): {…}

Eine Log-Datei zu LESEN ist kein Patchen. Was dadurch sichtbar wird — welches Werkzeug,
wie lange, mit welchem Ergebnis, in welchem Lauf — ist fuer "was tut sie gerade und wo
haengt es" oft nuetzlicher als der Fliesstext ihrer Gedanken.

## Es hat sich beim ersten Blick bezahlt gemacht

Der Parser lief gegen den echten Box-Log und meldete sofort:

  web_extract      2 Rufe   2 Fehler   <-- IMMER ROT
      DuckDuckGo (ddgs) is a search-only backend and cannot extract URL content.
  web_search      20 Rufe   0 Fehler   Schnitt 1,77 s

Nachgesehen: `web_extract` scheitert seit MINDESTENS dem 25.08. jeden Morgen um 07:00
mit derselben Meldung — im Daily-News-Cron, vier Tage lang, ohne dass es irgendwo
aufgefallen waere. Genau dafuer steht die Bilanz OBEN und die Zeitleiste darunter:
Ein Dauerfehler verschwindet in einer Ereignisliste, in der Zeile
"web_extract · 2 Rufe · 2 Fehler" nicht.

## Was die Ansicht NICHT verspricht

Denkstrom und Werkzeug-Argumente stehen nicht im Log und tauchen darum auch nicht auf.
Das steht so in der Ansicht selbst, nicht nur im Code — eine Oberflaeche, die mehr
andeutet als sie hat, ist schlimmer als eine, die ihre Grenze nennt.

## Umsetzung

  services/agent_aktivitaet.py   liest nur die letzten 512 kB (die Datei waechst auf
                                 MB und wird rotiert), vier gemessene Zeilenformen,
                                 Bilanz je Werkzeug + Gruppierung je Lauf
  GET /api/agent/aktivitaet      limit gedeckelt auf 300
  features/agent/Werkzeugverlauf.tsx   Bilanz -> Laeufe -> aufklappbare Zeitleiste

Fehlt das Log (Entwicklungsrechner ohne Hermes), verschwindet der Abschnitt still,
statt eine leere Karte zu zeigen — wie der Rest der Seite es haelt.

## Verifiziert

  Parser gegen den echten Box-Log: 26 Rufe, 4 Werkzeuge, 2 Laeufe erkannt,
    Rauschen (mem_trim, aiohttp) ignoriert
  Im Browser gegen dieselben Daten: "IMMER ROT"-Markierung sitzt, Grund im Klartext,
    Laufgruppen mit Zeitspanne, 26 Einzelrufe in der Zeitleiste
  7 neue Tests (44 gesamt) — sie pruefen gezielt die BILANZ-Karte, nicht irgendein
    Vorkommen des Werkzeugnamens; der steht auch in der Zeitleiste

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 09:51:14 +02:00

100 lines
4.5 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.
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(
<QueryClientProvider client={qc}>
<Werkzeugverlauf />
</QueryClientProvider>,
)
}
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:0507: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())
})
})