refactor: switch the whole project to English
talk is going to be published. Code comments, CLI output, error messages,
daemon log lines, MCP descriptions and results, test names and assertions,
README, spec, skill, build scripts and flake are now English. Only the
spoken announcement texts stay bilingual: the wrap result and the test
sentence are German or English depending on the detected language.
Language fallback is now configurable: `lang = "en" | "de"` (env
TALK_LANG, default en) is used when detection cannot tell (numbers, single
words, ties) and for talk's own announcements. langdetect.Detect takes the
fallback as an argument instead of hard-coding German. `talk setup` asks for
it; `talk doctor` and `talk status` show it.
Vocabulary pinned across packages so the process-boundary tests observe
stable strings: chain log lines ("chain: tier X skipped (not available)",
"falling back to the next tier", "spoken via X"), queue lines ("discarded:
quiet hours ... active", "replaced by toast", "max_age"), CLI notes
("speaking directly", "daemon started at", "no daemon", "nothing to cancel"),
status block with aligned English labels, MCP messages, origin guard 403.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
4d30bd95ad
commit
85fa64cfe6
95 files changed
+4707
-4854
No files matched your search
+86
-84
@@ -1,10 +1,10 @@
|
||||
package test
|
||||
|
||||
// Prozessgrenzen-Tests für den MCP-Endpoint (Task 06): der komplette
|
||||
// Handshake (initialize → notifications/initialized → tools/list →
|
||||
// tools/call) als rohe JSON-RPC-HTTP-Payloads gegen einen echten talkd —
|
||||
// literal, ohne Client-SDK. tools/call muss sich exakt wie POST /speak
|
||||
// verhalten: dieselbe Queue-Aufnahme inklusive Mute und Gesetz.
|
||||
// Process-boundary tests for the MCP endpoint (Task 06): the complete
|
||||
// handshake (initialize → notifications/initialized → tools/list →
|
||||
// tools/call) as raw JSON-RPC HTTP payloads against a real talkd — literal,
|
||||
// without a client SDK. tools/call must behave exactly like POST /speak: the
|
||||
// same queue admission including mute and law.
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -14,26 +14,26 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// mcpToolText liest content[0].text aus einem tools/call-Ergebnis.
|
||||
// mcpToolText reads content[0].text from a tools/call result.
|
||||
func mcpToolText(t *testing.T, m map[string]any) string {
|
||||
t.Helper()
|
||||
result, ok := m["result"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("tools/call ohne result: %v", m)
|
||||
t.Fatalf("tools/call without result: %v", m)
|
||||
}
|
||||
content, ok := result["content"].([]any)
|
||||
if !ok || len(content) == 0 {
|
||||
t.Fatalf("tools/call ohne content: %v", result)
|
||||
t.Fatalf("tools/call without content: %v", result)
|
||||
}
|
||||
first, ok := content[0].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("content[0] ist kein Objekt: %v", content[0])
|
||||
t.Fatalf("content[0] is not an object: %v", content[0])
|
||||
}
|
||||
text, _ := first["text"].(string)
|
||||
return text
|
||||
}
|
||||
|
||||
// mcpIsError liest isError (fehlt = false — bewusste Semantik).
|
||||
// mcpIsError reads isError (missing = false — deliberate semantics).
|
||||
func mcpIsError(m map[string]any) bool {
|
||||
if result, ok := m["result"].(map[string]any); ok {
|
||||
if e, ok := result["isError"].(bool); ok {
|
||||
@@ -43,72 +43,73 @@ func mcpIsError(m map[string]any) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// TestMCPHandshake (Abnahmekriterium): die curl-Sequenz initialize →
|
||||
// notifications/initialized → tools/list → tools/call sieht das speak-Tool
|
||||
// und enqueued erfolgreich — die Ansage läuft durch Queue und Fake-Provider.
|
||||
// TestMCPHandshake (acceptance criterion): the curl sequence initialize →
|
||||
// notifications/initialized → tools/list → tools/call sees the speak tool
|
||||
// and enqueues successfully — the announcement runs through the queue and
|
||||
// the fake provider.
|
||||
func TestMCPHandshake(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
d := startDaemon(t, root, fake.srv.URL, nil)
|
||||
|
||||
// 1. initialize: angefragte Version wird geechot; capabilities nennen
|
||||
// tools; serverInfo ist talkd.
|
||||
// 1. initialize: the requested version is echoed; capabilities name
|
||||
// tools; serverInfo is talkd.
|
||||
raw := mcpPost(t, d, `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl-test","version":"0"}}}`)
|
||||
if raw.status != http.StatusOK {
|
||||
t.Fatalf("initialize: Status %d — Body: %s", raw.status, raw.body)
|
||||
t.Fatalf("initialize: status %d — body: %s", raw.status, raw.body)
|
||||
}
|
||||
if ct := raw.header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
|
||||
t.Errorf("initialize Content-Type = %q, want application/json", ct)
|
||||
}
|
||||
m := mcpDecode(t, raw)
|
||||
if m["jsonrpc"] != "2.0" || fmt.Sprint(m["id"]) != "1" {
|
||||
t.Fatalf("initialize envelope = %v (jsonrpc/id prüfen)", m)
|
||||
t.Fatalf("initialize envelope = %v (check jsonrpc/id)", m)
|
||||
}
|
||||
result, ok := m["result"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("initialize ohne result: %v", m)
|
||||
t.Fatalf("initialize without result: %v", m)
|
||||
}
|
||||
if result["protocolVersion"] != "2025-06-18" {
|
||||
t.Errorf("protocolVersion = %v, want Echo der angefragten 2025-06-18", result["protocolVersion"])
|
||||
t.Errorf("protocolVersion = %v, want echo of the requested 2025-06-18", result["protocolVersion"])
|
||||
}
|
||||
caps, ok := result["capabilities"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("capabilities = %v, want Objekt", result["capabilities"])
|
||||
t.Fatalf("capabilities = %v, want object", result["capabilities"])
|
||||
}
|
||||
if _, ok := caps["tools"]; !ok {
|
||||
t.Errorf("capabilities ohne tools: %v", caps)
|
||||
t.Errorf("capabilities without tools: %v", caps)
|
||||
}
|
||||
serverInfo, _ := result["serverInfo"].(map[string]any)
|
||||
if serverInfo["name"] != "talkd" {
|
||||
t.Errorf("serverInfo.name = %v, want talkd", serverInfo["name"])
|
||||
}
|
||||
if v, _ := serverInfo["version"].(string); v == "" {
|
||||
t.Error("serverInfo.version ist leer")
|
||||
t.Error("serverInfo.version is empty")
|
||||
}
|
||||
|
||||
// Andere angefragte Version wird ebenso geechot (akzeptiert).
|
||||
// Another requested version is echoed as well (accepted).
|
||||
raw = mcpPost(t, d, `{"jsonrpc":"2.0","id":2,"method":"initialize","params":{"protocolVersion":"2025-03-26"}}`)
|
||||
m = mcpDecode(t, raw)
|
||||
if m["result"].(map[string]any)["protocolVersion"] != "2025-03-26" {
|
||||
t.Errorf("protocolVersion = %v, want Echo 2025-03-26", m["result"])
|
||||
t.Errorf("protocolVersion = %v, want echo 2025-03-26", m["result"])
|
||||
}
|
||||
|
||||
// 2. notifications/initialized: Notification → 202 ohne Body.
|
||||
// 2. notifications/initialized: notification → 202 without body.
|
||||
raw = mcpPost(t, d, `{"jsonrpc":"2.0","method":"notifications/initialized"}`)
|
||||
if raw.status != http.StatusAccepted {
|
||||
t.Fatalf("notifications/initialized: Status %d, want 202", raw.status)
|
||||
t.Fatalf("notifications/initialized: status %d, want 202", raw.status)
|
||||
}
|
||||
if raw.body != "" {
|
||||
t.Errorf("notifications/initialized Body = %q, want leer", raw.body)
|
||||
t.Errorf("notifications/initialized body = %q, want empty", raw.body)
|
||||
}
|
||||
|
||||
// 3. tools/list: genau das speak-Tool mit dem Brief-Schema.
|
||||
// 3. tools/list: exactly the speak tool with the briefed schema.
|
||||
raw = mcpPost(t, d, `{"jsonrpc":"2.0","id":3,"method":"tools/list"}`)
|
||||
m = mcpDecode(t, raw)
|
||||
result = m["result"].(map[string]any)
|
||||
tools, ok := result["tools"].([]any)
|
||||
if !ok || len(tools) != 1 {
|
||||
t.Fatalf("tools = %v, want genau ein Tool", result["tools"])
|
||||
t.Fatalf("tools = %v, want exactly one tool", result["tools"])
|
||||
}
|
||||
tool := tools[0].(map[string]any)
|
||||
if tool["name"] != "speak" {
|
||||
@@ -116,7 +117,7 @@ func TestMCPHandshake(t *testing.T) {
|
||||
}
|
||||
schema, ok := tool["inputSchema"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("inputSchema = %v, want Objekt", tool["inputSchema"])
|
||||
t.Fatalf("inputSchema = %v, want object", tool["inputSchema"])
|
||||
}
|
||||
if schema["type"] != "object" {
|
||||
t.Errorf("inputSchema.type = %v, want object", schema["type"])
|
||||
@@ -124,75 +125,76 @@ func TestMCPHandshake(t *testing.T) {
|
||||
props := schema["properties"].(map[string]any)
|
||||
for _, name := range []string{"text", "priority", "provider", "lang", "now"} {
|
||||
if _, ok := props[name]; !ok {
|
||||
t.Errorf("inputSchema ohne property %q: %v", name, props)
|
||||
t.Errorf("inputSchema without property %q: %v", name, props)
|
||||
}
|
||||
}
|
||||
if len(props) != 5 {
|
||||
t.Errorf("inputSchema hat %d properties (%v), want genau die 5 aus dem Brief", len(props), props)
|
||||
t.Errorf("inputSchema has %d properties (%v), want exactly the 5 from the brief", len(props), props)
|
||||
}
|
||||
if req, ok := schema["required"].([]any); !ok || len(req) != 1 || req[0] != "text" {
|
||||
t.Errorf("required = %v, want [text]", schema["required"])
|
||||
}
|
||||
|
||||
// 4. tools/call: Aufnahme wie POST /speak — "queued: <id>" und die
|
||||
// Ansage taucht am Fake-Provider auf (gleiche Queue, kein Parallelweg).
|
||||
raw = mcpPost(t, d, `{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"speak","arguments":{"text":"hallo über mcp"}}}`)
|
||||
// 4. tools/call: admission like POST /speak — "queued: <id>" and the
|
||||
// announcement shows up at the fake provider (same queue, no
|
||||
// parallel path).
|
||||
raw = mcpPost(t, d, `{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"speak","arguments":{"text":"hello via mcp"}}}`)
|
||||
if raw.status != http.StatusOK {
|
||||
t.Fatalf("tools/call: Status %d — Body: %s", raw.status, raw.body)
|
||||
t.Fatalf("tools/call: status %d — body: %s", raw.status, raw.body)
|
||||
}
|
||||
m = mcpDecode(t, raw)
|
||||
if fmt.Sprint(m["id"]) != "4" {
|
||||
t.Errorf("tools/call id = %v, want Echo 4", m["id"])
|
||||
t.Errorf("tools/call id = %v, want echo 4", m["id"])
|
||||
}
|
||||
text := mcpToolText(t, m)
|
||||
if !strings.HasPrefix(text, "queued: ") || len(text) <= len("queued: ") {
|
||||
t.Fatalf("tools/call text = %q, want \"queued: <id>\"", text)
|
||||
}
|
||||
if mcpIsError(m) {
|
||||
t.Errorf("tools/call isError = true bei erfolgreicher Aufnahme: %v", m)
|
||||
t.Errorf("tools/call isError = true on successful admission: %v", m)
|
||||
}
|
||||
hits := fake.waitCompleted(t, 1)
|
||||
if hits[0].Text != "hallo über mcp" {
|
||||
t.Errorf("Fake-Treffer = %q, will die MCP-Ansage", hits[0].Text)
|
||||
if hits[0].Text != "hello via mcp" {
|
||||
t.Errorf("fake hit = %q, want the MCP announcement", hits[0].Text)
|
||||
}
|
||||
}
|
||||
|
||||
// TestMCPToolsCallGesetze: muted und Gesetz-Verwürfe sind Tool-ERGEBNISSE
|
||||
// (isError:false mit erklärendem Text), keine Protokollfehler — und die
|
||||
// Ansage bleibt stumm.
|
||||
func TestMCPToolsCallGesetze(t *testing.T) {
|
||||
// TestMCPToolsCallLaws: muted and law discards are tool RESULTS
|
||||
// (isError:false with explanatory text), not protocol errors — and the
|
||||
// announcement stays silent.
|
||||
func TestMCPToolsCallLaws(t *testing.T) {
|
||||
t.Run("muted", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
d := startDaemon(t, root, fake.srv.URL, nil)
|
||||
postPath(t, d, "/mute")
|
||||
|
||||
m := mcpDecode(t, mcpPost(t, d, `{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"speak","arguments":{"text":"stille bitte"}}}`))
|
||||
m := mcpDecode(t, mcpPost(t, d, `{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"speak","arguments":{"text":"silence please"}}}`))
|
||||
if mcpIsError(m) {
|
||||
t.Fatalf("muted ist isError:true — soll Ergebnis sein: %v", m)
|
||||
t.Fatalf("muted is isError:true — should be a result: %v", m)
|
||||
}
|
||||
if text := mcpToolText(t, m); !strings.HasPrefix(text, "muted") {
|
||||
t.Errorf("text = %q, will muted-Erklärung", text)
|
||||
t.Errorf("text = %q, want the muted explanation", text)
|
||||
}
|
||||
fake.assertNoHits(t, 300*time.Millisecond)
|
||||
})
|
||||
|
||||
t.Run("rate_limit verwirft low", func(t *testing.T) {
|
||||
t.Run("rate_limit discards low", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
d := startDaemon(t, root, fake.srv.URL, map[string]string{"TALK_RATE_LIMIT": "30s"})
|
||||
|
||||
// Erste low-Ansage spielt (Fenster frei) und bucht es.
|
||||
m := mcpDecode(t, mcpPost(t, d, `{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"speak","arguments":{"text":"erste","priority":"low"}}}`))
|
||||
// The first low announcement plays (window free) and books it.
|
||||
m := mcpDecode(t, mcpPost(t, d, `{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"speak","arguments":{"text":"first","priority":"low"}}}`))
|
||||
if text := mcpToolText(t, m); !strings.HasPrefix(text, "queued: ") {
|
||||
t.Fatalf("erste low: text = %q, want queued", text)
|
||||
t.Fatalf("first low: text = %q, want queued", text)
|
||||
}
|
||||
fake.waitCompleted(t, 1)
|
||||
|
||||
// Zweite low innerhalb des Fensters: Verwurf als Ergebnis.
|
||||
m = mcpDecode(t, mcpPost(t, d, `{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"speak","arguments":{"text":"zweite","priority":"low"}}}`))
|
||||
// Second low inside the window: discard as a result.
|
||||
m = mcpDecode(t, mcpPost(t, d, `{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"speak","arguments":{"text":"second","priority":"low"}}}`))
|
||||
if mcpIsError(m) {
|
||||
t.Fatalf("Verwurf ist isError:true — soll Ergebnis sein: %v", m)
|
||||
t.Fatalf("discard is isError:true — should be a result: %v", m)
|
||||
}
|
||||
if text := mcpToolText(t, m); !strings.HasPrefix(text, "discarded: rate_limit") {
|
||||
t.Errorf("text = %q, want discarded: rate_limit …", text)
|
||||
@@ -200,16 +202,16 @@ func TestMCPToolsCallGesetze(t *testing.T) {
|
||||
fake.assertNoFurtherArrivals(t, 300*time.Millisecond)
|
||||
})
|
||||
|
||||
t.Run("quiet_hours verwirft", func(t *testing.T) {
|
||||
t.Run("quiet_hours discards", func(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
d := startDaemon(t, root, fake.srv.URL, map[string]string{
|
||||
"TALK_QUIET_HOURS": fensterUm(),
|
||||
"TALK_QUIET_HOURS": windowAround(),
|
||||
})
|
||||
|
||||
m := mcpDecode(t, mcpPost(t, d, `{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"speak","arguments":{"text":"nachts"}}}`))
|
||||
m := mcpDecode(t, mcpPost(t, d, `{"jsonrpc":"2.0","id":10,"method":"tools/call","params":{"name":"speak","arguments":{"text":"at night"}}}`))
|
||||
if mcpIsError(m) {
|
||||
t.Fatalf("Ruhezeit-Verwurf ist isError:true — soll Ergebnis sein: %v", m)
|
||||
t.Fatalf("quiet-hours discard is isError:true — should be a result: %v", m)
|
||||
}
|
||||
if text := mcpToolText(t, m); !strings.HasPrefix(text, "discarded: quiet_hours") {
|
||||
t.Errorf("text = %q, want discarded: quiet_hours …", text)
|
||||
@@ -218,10 +220,10 @@ func TestMCPToolsCallGesetze(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
// TestMCPFehlerPfade: Validierungsfehler als JSON-RPC-Fehler (-32602),
|
||||
// unbekannte Methode -32601, kaputtes JSON -32700, falsche jsonrpc-Version
|
||||
// und Batches -32600, GET /mcp 405 (keine server-initiierten Streams).
|
||||
func TestMCPFehlerPfade(t *testing.T) {
|
||||
// TestMCPErrorPaths: validation errors as JSON-RPC errors (-32602), unknown
|
||||
// method -32601, broken JSON -32700, wrong jsonrpc version and batches
|
||||
// -32600, GET /mcp 405 (no server-initiated streams).
|
||||
func TestMCPErrorPaths(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
d := startDaemon(t, root, fake.srv.URL, nil)
|
||||
@@ -232,34 +234,34 @@ func TestMCPFehlerPfade(t *testing.T) {
|
||||
code float64
|
||||
msgPart string
|
||||
}{
|
||||
{"text fehlt", `{"jsonrpc":"2.0","id":21,"method":"tools/call","params":{"name":"speak","arguments":{}}}`, -32602, "text"},
|
||||
{"text leer", `{"jsonrpc":"2.0","id":22,"method":"tools/call","params":{"name":"speak","arguments":{"text":" "}}}`, -32602, "text"},
|
||||
{"ungültige priority", `{"jsonrpc":"2.0","id":23,"method":"tools/call","params":{"name":"speak","arguments":{"text":"x","priority":"mega"}}}`, -32602, "priority"},
|
||||
{"unbekanntes Tool", `{"jsonrpc":"2.0","id":24,"method":"tools/call","params":{"name":"sing","arguments":{"text":"x"}}}`, -32602, "sing"},
|
||||
{"unbekannte Methode", `{"jsonrpc":"2.0","id":25,"method":"resources/list"}`, -32601, "resources/list"},
|
||||
{"kaputtes JSON", `{"jsonrpc":"2.0","id":`, -32700, "JSON"},
|
||||
{"falsche jsonrpc-Version", `{"jsonrpc":"1.0","id":26,"method":"ping"}`, -32600, "2.0"},
|
||||
{"Batch (seit 2025-06-18 entfernt)", `[{"jsonrpc":"2.0","id":27,"method":"ping"}]`, -32600, "Batch"},
|
||||
{"text missing", `{"jsonrpc":"2.0","id":21,"method":"tools/call","params":{"name":"speak","arguments":{}}}`, -32602, "text"},
|
||||
{"text empty", `{"jsonrpc":"2.0","id":22,"method":"tools/call","params":{"name":"speak","arguments":{"text":" "}}}`, -32602, "text"},
|
||||
{"invalid priority", `{"jsonrpc":"2.0","id":23,"method":"tools/call","params":{"name":"speak","arguments":{"text":"x","priority":"mega"}}}`, -32602, "priority"},
|
||||
{"unknown tool", `{"jsonrpc":"2.0","id":24,"method":"tools/call","params":{"name":"sing","arguments":{"text":"x"}}}`, -32602, "sing"},
|
||||
{"unknown method", `{"jsonrpc":"2.0","id":25,"method":"resources/list"}`, -32601, "resources/list"},
|
||||
{"broken JSON", `{"jsonrpc":"2.0","id":`, -32700, "JSON"},
|
||||
{"wrong jsonrpc version", `{"jsonrpc":"1.0","id":26,"method":"ping"}`, -32600, "2.0"},
|
||||
{"batch (removed since 2025-06-18)", `[{"jsonrpc":"2.0","id":27,"method":"ping"}]`, -32600, "batch"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
raw := mcpPost(t, d, tc.payload)
|
||||
if raw.status != http.StatusOK {
|
||||
t.Fatalf("Status = %d, want 200 (JSON-RPC-Fehler im Body)", raw.status)
|
||||
t.Fatalf("status = %d, want 200 (JSON-RPC error in the body)", raw.status)
|
||||
}
|
||||
m := mcpDecode(t, raw)
|
||||
rpcErr, ok := m["error"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("kein error-Objekt: %v", m)
|
||||
t.Fatalf("no error object: %v", m)
|
||||
}
|
||||
if rpcErr["code"] != tc.code {
|
||||
t.Errorf("code = %v, want %v (Body: %s)", rpcErr["code"], tc.code, raw.body)
|
||||
t.Errorf("code = %v, want %v (body: %s)", rpcErr["code"], tc.code, raw.body)
|
||||
}
|
||||
if msg, _ := rpcErr["message"].(string); !strings.Contains(msg, tc.msgPart) {
|
||||
t.Errorf("message = %q, will Teil %q", msg, tc.msgPart)
|
||||
t.Errorf("message = %q, want part %q", msg, tc.msgPart)
|
||||
}
|
||||
if _, hasResult := m["result"]; hasResult {
|
||||
t.Errorf("error-Antwort mit result: %v", m)
|
||||
t.Errorf("error response with result: %v", m)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -267,34 +269,34 @@ func TestMCPFehlerPfade(t *testing.T) {
|
||||
t.Run("ping", func(t *testing.T) {
|
||||
m := mcpDecode(t, mcpPost(t, d, `{"jsonrpc":"2.0","id":28,"method":"ping"}`))
|
||||
if _, ok := m["result"]; !ok {
|
||||
t.Fatalf("ping ohne result: %v", m)
|
||||
t.Fatalf("ping without result: %v", m)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("GET /mcp → 405 (keine server-initiierten Streams)", func(t *testing.T) {
|
||||
t.Run("GET /mcp → 405 (no server-initiated streams)", func(t *testing.T) {
|
||||
raw := doRequest(t, http.MethodGet, d.baseURL+"/mcp", nil, "")
|
||||
if raw.status != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("GET /mcp Status = %d, want 405", raw.status)
|
||||
t.Fatalf("GET /mcp status = %d, want 405", raw.status)
|
||||
}
|
||||
if allow := raw.header.Get("Allow"); !strings.Contains(allow, "POST") {
|
||||
t.Errorf("Allow = %q, will POST nennen", allow)
|
||||
t.Errorf("Allow = %q, want POST named", allow)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Notification mit unbekannter Methode → still 202", func(t *testing.T) {
|
||||
t.Run("notification with unknown method → silent 202", func(t *testing.T) {
|
||||
raw := mcpPost(t, d, `{"jsonrpc":"2.0","method":"notifications/roots/list_changed"}`)
|
||||
if raw.status != http.StatusAccepted || raw.body != "" {
|
||||
t.Fatalf("Status %d Body %q, want 202 ohne Body", raw.status, raw.body)
|
||||
t.Fatalf("status %d body %q, want 202 without body", raw.status, raw.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestStatusMCPFeld: /status meldet mcp:true (Compat-Feld Task 06).
|
||||
func TestStatusMCPFeld(t *testing.T) {
|
||||
// TestStatusMCPField: /status reports mcp:true (compat field Task 06).
|
||||
func TestStatusMCPField(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
fake := newFakeProvider(t, 0)
|
||||
d := startDaemon(t, root, fake.srv.URL, nil)
|
||||
if st := getStatus(t, d); !st.MCP {
|
||||
t.Error("mcp = false, want true (Endpoint ist vorhanden)")
|
||||
t.Error("mcp = false, want true (endpoint is present)")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user