Files
mission-control-v2/frontend/src/app/palette/register.test.ts
T
HitonabiandClaude Opus 5 24a0694b80 feat(palette): P6 — die Palette wird zum Bedienwerkzeug (§4.6)
Bisher listete Strg+K die elf Navigations-Eintraege: ein Sprungbrett. Jetzt tippt
man, WAS man will, nicht wo es liegt.

  Gehe zu   die 11 Ziele — direkt aus NAV, damit es nie zwei Listen gibt
  Tun       Wartung/Logs oeffnen, Updates suchen, Snapshot, Motor neu starten
  Finden    das Getippte in Chronik oder Wissens-Vault suchen
  An Lucy   das Getippte als Idee erfassen

## Die Sicherheitsregel ist verdrahtet, nicht nur beschrieben

Die Palette ist schnell genug, dass "Strg+K, mot, Enter" den Motor neu startet,
bevor man es zu Ende gedacht hat. Jeder eingreifende Befehl traegt darum
`rueckfrage`; die Oberflaeche haelt dann an und sagt erst, was passieren WIRD:

  "Alle geladenen Modelle fallen dabei aus dem Speicher und muessen neu aufwaermen —
   das dauert je nach Modell ueber 20 Sekunden. Laufende Antworten brechen ab."

Ein Test prueft die Regel generisch: Jeder Befehl, dessen Label auf "neu starten",
"loeschen", "deploy", "reboot" o. AE. passt, MUSS eine Rueckfrage haben. Wer kuenftig
einen eingreifenden Befehl ohne Rueckfrage ergaenzt, macht die Ampel rot.

"An Lucy" erfasst mit `art: "idee"` — durchdenken, nicht bauen. `"projekt"` wuerde
sofort ein Repo anlegen; das darf aus einem Tastendruck nie passieren. Auch das ist
getestet.

## Die "Finden"-Eintraege fuehren nicht ins Leere

Sie brauchten erst ein Ziel — sonst haette ich genau die leere Leitung gebaut, die ich
beim Werkzeug-Verlauf vermieden habe:

  ChronikView   neues Suchfeld, filtert ueber Betreff/Text/Quelle, Wort in der URL
                (/chronik?q=…), Trefferzahl in der Ueberschrift, "Filter aufheben"
                im Leer-Zustand
  WissenView    hatte das Suchfeld schon, aber nur lokal — jetzt aus /wissen?suche=
                gespeist und damit teilbar

## Verifiziert im Browser

  Strg+K oeffnet, "Gehe zu" (11) + "Tun" (5); leere Gruppen bleiben verborgen
  Text tippen -> "Finden" (2) und "An Lucy" (1) erscheinen
  "Motor neu starten" traegt den Chip "fragt nach"; Auswahl zeigt die Rueckfrage
    mit vollem Folgentext — es wird NICHTS gestartet
  "Abbrechen" fuehrt zurueck in die Liste, keine Meldung, kein Aufruf

54 Tests gruen (10 neue) · ESLint 0 Fehler · Einstieg 118 762 B gzip / Budget 125 000.

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

105 lines
4.7 KiB
TypeScript

import { describe, expect, it, vi } from "vitest"
import { befehle, suchtext, type BefehlsKontext } from "./register"
import { NAV } from "@/nav"
function kontext(ueberschreiben: Partial<BefehlsKontext> = {}): BefehlsKontext {
return {
text: "",
navigate: vi.fn(),
schublade: vi.fn(),
melden: vi.fn(),
api: vi.fn(async () => ({}) as never),
...ueberschreiben,
}
}
describe("Befehls-Register", () => {
it("führt jedes Navigations-Ziel auf — genau einmal", () => {
const ziele = befehle("").filter((b) => b.gruppe === "gehe")
expect(ziele).toHaveLength(NAV.length)
expect(new Set(ziele.map((z) => z.id)).size).toBe(NAV.length)
})
it("zeigt Suche und „An Lucy“ erst, wenn etwas getippt ist", () => {
const leer = befehle("")
expect(leer.some((b) => b.gruppe === "finden")).toBe(false)
expect(leer.some((b) => b.gruppe === "lucy")).toBe(false)
const getippt = befehle("Wissensgraph-Ansicht bauen")
expect(getippt.filter((b) => b.gruppe === "finden")).toHaveLength(2)
expect(getippt.filter((b) => b.gruppe === "lucy")).toHaveLength(1)
})
// Unter acht Zeichen ist es kein Auftrag, sondern ein Tippfehler auf dem Weg zu
// einem Sprungziel. Sonst böte die Palette bei „mod" an, daraus eine Idee zu machen.
it("bietet kurze Eingaben NICHT als Idee an", () => {
expect(befehle("mod").some((b) => b.gruppe === "lucy")).toBe(false)
expect(befehle("modelle").some((b) => b.gruppe === "lucy")).toBe(false)
expect(befehle("modellwechsel").some((b) => b.gruppe === "lucy")).toBe(true)
})
// ── Die Sicherheitsregel ──────────────────────────────────────────────────
// Die Palette ist schnell genug, dass „Strg+K, mot, Enter" einen Dienst neu startet,
// bevor man es zu Ende gedacht hat. Alles Eingreifende MUSS deshalb `rueckfrage`
// tragen — die Oberfläche hält dann an und sagt erst, was passieren wird.
it("verlangt für jeden eingreifenden Befehl eine Rückfrage", () => {
const eingreifend = /neu starten|löschen|loeschen|deploy|zurücksetzen|reboot|entfernen/i
for (const b of befehle("etwas getipptes")) {
if (eingreifend.test(b.label)) {
expect(b.rueckfrage, `„${b.label}" muss nachfragen`).toBeTruthy()
}
}
})
it("nennt in der Rückfrage die Folge, nicht nur die Aktion", () => {
const motor = befehle("").find((b) => b.id === "tun:engine-neustart")!
expect(motor.rueckfrage).toBeTruthy()
// Der Text muss sagen, was der Nutzer DANACH merkt.
expect(motor.rueckfrage).toMatch(/Modelle/)
expect(motor.rueckfrage).toMatch(/aufwärmen|abbrechen|abbrechen/i)
})
it("lässt harmlose Befehle ohne Rückfrage durch", () => {
const wartung = befehle("").find((b) => b.id === "tun:wartung")!
expect(wartung.rueckfrage).toBeUndefined()
})
// ── Verhalten einzelner Befehle ───────────────────────────────────────────
it("öffnet die Schublade über die URL, nicht über ein Ereignis", () => {
const ctx = kontext()
befehle("").find((b) => b.id === "tun:logs")!.ausfuehren(ctx)
expect(ctx.schublade).toHaveBeenCalledWith("logs")
})
it("erfasst eine Idee zum Durchdenken — und startet keinen Bau", async () => {
const ctx = kontext({ text: "Wissensgraph-Ansicht bauen" })
await befehle(ctx.text).find((b) => b.gruppe === "lucy")!.ausfuehren(ctx)
expect(ctx.api).toHaveBeenCalledTimes(1)
const [pfad, init] = (ctx.api as ReturnType<typeof vi.fn>).mock.calls[0]
expect(pfad).toBe("/api/ideen")
const rumpf = JSON.parse((init as RequestInit).body as string)
// `art: "idee"` heisst durchdenken. `"projekt"` wuerde sofort ein Repo anlegen —
// das darf die Palette niemals aus einem Tastendruck heraus tun.
expect(rumpf.art).toBe("idee")
expect(rumpf.titel).toBe("Wissensgraph-Ansicht bauen")
})
it("kürzt einen überlangen Ideen-Titel und hebt den vollen Text in die Notiz", async () => {
const lang = "a".repeat(200)
const ctx = kontext({ text: lang })
await befehle(lang).find((b) => b.gruppe === "lucy")!.ausfuehren(ctx)
const rumpf = JSON.parse(
((ctx.api as ReturnType<typeof vi.fn>).mock.calls[0][1] as RequestInit).body as string,
)
expect(rumpf.titel.length).toBeLessThanOrEqual(160)
expect(rumpf.notiz).toBe(lang)
})
it("packt Suchwörter in den Suchtext, damit cmdk sie findet", () => {
const wartung = befehle("").find((b) => b.id === "tun:wartung")!
expect(suchtext(wartung)).toMatch(/neustart/) // steht nur in den Stichworten
expect(suchtext(wartung)).toMatch(/System-Wartung/)
})
})