package api import ( "net/http" "strconv" "testing" ) // TestAgentDisabledWithoutAKey — an instance with no API key must start, serve // the app, and not offer the assistant. // // The contract CHANGED with #79: the chat route is now always registered (so a // settings change can turn the assistant on without a restart), so "off" is a // runtime 503 rather than a missing route. capabilities reports agent:false, and // the frontend keys the chat tab off that — so a user never reaches the 503. func TestAgentDisabledWithoutAKey(t *testing.T) { r := authEngine(t, localCfg()) // localCfg has no agent configuration cookie := registerAndCookie(t, r, "noagent@example.com") gid := createGardenAPI(t, r, cookie, "G") // Chat is refused, plainly, because there is no Runner to run. w := doJSON(t, r, http.MethodPost, "/api/v1/agent/chat", map[string]any{"gardenId": gid, "message": "plant garlic"}, cookie) if w.Code != http.StatusServiceUnavailable { t.Errorf("chat without a key: status %d, want 503", w.Code) } // Capabilities advertises the assistant as unavailable, which is what the UI // actually consults. w = doJSON(t, r, http.MethodGet, "/api/v1/capabilities", nil, cookie) if w.Code != http.StatusOK { t.Fatalf("capabilities: status %d", w.Code) } if agent, _ := decodeMap(t, w.Body.Bytes())["agent"].(bool); agent { t.Error("capabilities reported agent:true with no key") } // History is just stored data behind the ordinary garden-role check, so it // reads fine (empty) whether or not a Runner exists — it isn't gated on one. path := "/api/v1/gardens/" + strconv.FormatInt(gid, 10) + "/agent/history" if w := doJSON(t, r, http.MethodGet, path, nil, cookie); w.Code != http.StatusOK { t.Errorf("history without a key: status %d, want 200 (it's data, not the model)", w.Code) } // And the rest of the app is entirely unaffected. if w := doJSON(t, r, http.MethodGet, fullPath(gid), nil, cookie); w.Code != http.StatusOK { t.Errorf("editor load: status %d, want 200 — an unconfigured agent must not break the app", w.Code) } } // TestChatRejectsAMalformedToday — the sender's local date is validated before // anything else about the request, assistant or no assistant: a bad body is a // 400 either way, so a client can't mistake its own bad date for the assistant // being off. func TestChatRejectsAMalformedToday(t *testing.T) { r := authEngine(t, localCfg()) cookie := registerAndCookie(t, r, "dates@example.com") gid := createGardenAPI(t, r, cookie, "G") w := doJSON(t, r, http.MethodPost, "/api/v1/agent/chat", map[string]any{"gardenId": gid, "message": "plant garlic", "today": "Aug 22"}, cookie) if w.Code != http.StatusBadRequest { t.Errorf("chat with today=%q: status %d, want 400", "Aug 22", w.Code) } // A well-formed date (or none) gets past validation to the runner check. for _, today := range []string{"2026-08-22", ""} { w := doJSON(t, r, http.MethodPost, "/api/v1/agent/chat", map[string]any{"gardenId": gid, "message": "plant garlic", "today": today}, cookie) if w.Code != http.StatusServiceUnavailable { t.Errorf("chat with today=%q: status %d, want 503 (no runner configured)", today, w.Code) } } }