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>
100 lines
4.5 KiB
TypeScript
100 lines
4.5 KiB
TypeScript
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: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())
|
||
})
|
||
})
|