package main import ( "encoding/json" "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "regexp" "strconv" "strings" "testing" "time" ) func TestSubjectsReceiveStableIsolatedSlots(t *testing.T) { statePath := filepath.Join(t.TempDir(), "tenants.json") router, err := newTenantRouter(statePath, 2, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } first, err := router.slotFor("keycloak-subject-a") if err != nil { t.Fatal(err) } again, _ := router.slotFor("keycloak-subject-a") second, err := router.slotFor("keycloak-subject-b") if err != nil { t.Fatal(err) } if first != again || first == second { t.Fatalf("unexpected slots: first=%d again=%d second=%d", first, again, second) } if _, err := router.slotFor("keycloak-subject-c"); err == nil { t.Fatal("expected the fixed private tenant pool to report capacity") } reloaded, err := newTenantRouter(statePath, 2, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } persisted, _ := reloaded.slotFor("keycloak-subject-a") if persisted != first { t.Fatalf("assignment changed after reload: %d != %d", persisted, first) } } func TestRouterBlocksAdministrationAndRequiresIdentity(t *testing.T) { router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "http://127.0.0.1" }) if err != nil { t.Fatal(err) } for _, test := range []struct { method string path string user string want int }{ {http.MethodGet, "/", "", http.StatusUnauthorized}, {http.MethodPost, "/api/providers", "subject", http.StatusForbidden}, {http.MethodPost, "/api/commands/exec", "subject", http.StatusForbidden}, {http.MethodGet, "/api/dashboard/config", "subject", http.StatusForbidden}, {http.MethodGet, "/api/env", "subject", http.StatusForbidden}, } { request := httptest.NewRequest(test.method, test.path, nil) request.Header.Set("X-Forwarded-User", test.user) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != test.want { t.Fatalf("%s %s: got %d, want %d", test.method, test.path, response.Code, test.want) } } } func TestRouterRetiresStaleServiceWorkerWithoutIdentity(t *testing.T) { router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/sw.js?v=exp-v0.52.181", nil) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("got status %d", response.Code) } if response.Header().Get("Service-Worker-Allowed") != "/" { t.Fatal("service worker retirement response does not cover the origin") } if !strings.Contains(response.Header().Get("Cache-Control"), "no-store") { t.Fatal("service worker retirement response is cacheable") } if !strings.Contains(response.Header().Get("Clear-Site-Data"), "storage") { t.Fatal("service worker retirement response does not clear stale storage") } if !strings.Contains(response.Body.String(), "registration.unregister") { t.Fatal("service worker retirement script does not unregister itself") } } func TestRouterProxiesWebUIAndAddsTelegramShortcut(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.Header.Get("X-Forwarded-User") != "" || request.Header.Get("X-Auth-Request-User") != "" { t.Fatal("external identity header leaked to tenant backend") } if request.Header.Get(trustedTenantHeader) != "slot-0" { t.Fatalf("trusted tenant identity was not asserted: %q", request.Header.Get(trustedTenantHeader)) } if request.Header.Get("X-Hux-Subject") != "" || request.Header.Get("X-Hux-Trust") != "" || request.Header.Get("X-Hux-Relay-Key") != "" { t.Fatal("browser-supplied HUX trust headers reached the tenant backend") } cookies := request.Cookies() if len(cookies) != 2 || cookies[0].Name != tenantSessionCookie || cookies[1].Name != tenantProfileCookie { t.Fatalf("unexpected backend cookies: %#v", cookies) } writer.Header().Set("Content-Type", "text/html; charset=utf-8") _, _ = io.WriteString(writer, "Hermes WebUI") })) defer backend.Close() router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL }) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/", nil) request.Header.Set("X-Forwarded-User", "subject") request.Header.Set("X-Auth-Request-User", "subject") request.Header.Set("X-Hux-Subject", "usr_0123456789abcdef") request.Header.Set("X-Hux-Trust", "worker") request.Header.Set("X-Hux-Relay-Key", "browser-forgery") request.Header.Set("Cookie", "_oauth2_proxy=secret; "+tenantSessionCookie+"=session; "+tenantProfileCookie+"=default") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("got status %d", response.Code) } if !strings.Contains(response.Body.String(), "hermes-chat-bridge.js") || !strings.Contains(response.Body.String(), "hermes-chat-bridge.css") { t.Fatal("Telegram shortcut assets were not injected") } if !strings.Contains(response.Body.String(), "hermes-session-continuity.js") { t.Fatal("session continuity fallback was not injected") } assetRequest := httptest.NewRequest(http.MethodGet, "/hermes-chat-bridge.js", nil) assetRequest.Header.Set("X-Forwarded-User", "subject") assetResponse := httptest.NewRecorder() router.ServeHTTP(assetResponse, assetRequest) asset := assetResponse.Body.String() if !strings.Contains(asset, "hermes-files-sidebar") || !strings.Contains(asset, "hermes-telegram-sidebar") { t.Fatal("Files and Telegram were not integrated into the existing sidebar") } if !strings.Contains(asset, "document.querySelector('.rail')") || !strings.Contains(asset, "data-tooltip', 'Telegram") { t.Fatal("Telegram was not integrated as a native WebUI navigation action") } if strings.Contains(asset, "if (!files) return") { t.Fatal("Telegram navigation still depends on the removed legacy Files link") } if strings.Contains(asset, "position:fixed") || strings.Contains(asset, "hermes-chat-tools") { t.Fatal("legacy floating chat controls remain in the mobile bridge") } } func TestSessionContinuityAssetHasAccessiblePollFallback(t *testing.T) { router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(int) string { return "" }) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/hermes-session-continuity.js", nil) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("got status %d", response.Code) } asset := response.Body.String() for _, expected := range []string{ "aria-live", "aria-busy", "/api/session?session_id=", "&messages=1&msg_limit=24", "hermes_fallback=1", "Latest stored activity", "no renderable messages", "Session updates disconnected", "oauth2/start?rd=", "payload.session", "session_profile_mismatch", "session.is_streaming", "latest.observed", "addEventListener('online'", "addEventListener('pageshow'", "schedule(document.hidden", "fetch(", } { if !strings.Contains(asset, expected) { t.Fatalf("session fallback omitted %q", expected) } } for _, forbidden := range []string{ "location.replace", "location.reload", "history.replaceState", "WebSocket", // `/api/sessions/` belongs to the Hermes agent dashboard, not to the // tenant WebUI this router proxies. Polling it 404s on every request and // renders a false ownership error after each full page load. "/api/sessions/", } { if strings.Contains(asset, forbidden) { t.Fatalf("session fallback interferes with native continuity via %q", forbidden) } } } // tenantWebUI mirrors the dispatch contract of the deployed Hermes WebUI // (`hermes-webui` server.py): `GET /api/session?session_id=` is the only // session read route, an unrouted path falls through to a generic 404, and an // unknown session id is reported as "Session not found". type tenantWebUI struct { server *httptest.Server sessions map[string]bool identities []string requests int cookies []string } func newTenantWebUI(t *testing.T, marker string, sessions ...string) *tenantWebUI { t.Helper() backend := &tenantWebUI{sessions: map[string]bool{}} for _, id := range sessions { backend.sessions[id] = true } backend.server = httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { backend.requests++ backend.identities = append(backend.identities, request.Header.Get(trustedTenantHeader)) backend.cookies = append(backend.cookies, request.Header.Get("Cookie")) for _, leaked := range []string{"X-Forwarded-User", "X-Auth-Request-User", "Authorization"} { if request.Header.Get(leaked) != "" { t.Errorf("external identity header %q reached a tenant backend", leaked) } } writer.Header().Set("Content-Type", "application/json") if request.URL.Path != "/api/session" { writer.WriteHeader(http.StatusNotFound) _, _ = io.WriteString(writer, `{"error":"not found"}`) return } id := request.URL.Query().Get("session_id") if !backend.sessions[id] { writer.WriteHeader(http.StatusNotFound) _, _ = io.WriteString(writer, `{"error":"Session not found"}`) return } _ = json.NewEncoder(writer).Encode(map[string]any{"session": map[string]any{ "session_id": id, "title": marker, "messages": []map[string]any{{"role": "assistant", "content": marker}}, "message_count": 1, "is_streaming": false, }}) })) t.Cleanup(backend.server.Close) return backend } // continuityPollPath derives the request the injected fallback actually issues, // so a regression in the polled contract fails these tests instead of silently // reintroducing the permanent 404. func continuityPollPath(t *testing.T, router *tenantRouter, sessionID string) string { t.Helper() request := httptest.NewRequest(http.MethodGet, "/hermes-session-continuity.js", nil) request.Header.Set("X-Forwarded-User", "asset-reader") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("continuity asset got status %d", response.Code) } matcher := regexp.MustCompile(`'(/api/[^']*)' \+ encodeURIComponent\(sessionId\) \+\s*'([^']*)'`) parts := matcher.FindStringSubmatch(response.Body.String()) if parts == nil { t.Fatal("continuity fallback does not build a session-scoped poll URL") } return parts[1] + url.QueryEscape(sessionID) + parts[2] } func continuityPoll(t *testing.T, router *tenantRouter, subject, cookie, path string) *httptest.ResponseRecorder { t.Helper() request := httptest.NewRequest(http.MethodGet, path, nil) request.Header.Set("X-Forwarded-User", subject) request.Header.Set("X-Auth-Request-User", subject) if cookie != "" { request.Header.Set("Cookie", cookie) } response := httptest.NewRecorder() router.ServeHTTP(response, request) return response } // Reproduction of the reported failure. The tenant WebUI image // (registry.bstein.dev/bstein/hermes-webui@sha256:c276a9e1…) routes exactly one // session read, `GET /api/session?session_id=`; `/api/sessions/[/messages]` // belongs to the separate Hermes agent dashboard and falls through to // server.py's generic 404. Polling it could never succeed for anyone, so the // banner fired on every full page load rather than on a real ownership problem. func TestLegacyDashboardSessionPollAlwaysMissesTheTenantWebUI(t *testing.T) { const sessionID = "sess-1" backend := newTenantWebUI(t, "brad-private-session", sessionID) router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(int) string { return backend.server.URL }) if err != nil { t.Fatal(err) } for _, legacy := range []string{ "/api/sessions/" + sessionID, "/api/sessions/" + sessionID + "/messages?limit=24&hermes_fallback=1", } { response := continuityPoll(t, router, "keycloak-subject-brad", "", legacy) if response.Code != http.StatusNotFound { t.Fatalf("%s: got %d, want the WebUI's 404 for an unrouted path", legacy, response.Code) } } if response := continuityPoll(t, router, "keycloak-subject-brad", "", continuityPollPath(t, router, sessionID)); response.Code != http.StatusOK { t.Fatalf("the shipped fallback still misses the tenant WebUI: got %d", response.Code) } } // The Keycloak logout/login round-trip returns the browser to /session/ as // a full document load, which is the only moment the injected fallback runs. // The durable session must still resolve for its stable owner. func TestSessionContinuityPollSurvivesLogoutAndRelogin(t *testing.T) { const subject = "keycloak-subject-brad" const sessionID = "9048e2a574d1" backend := newTenantWebUI(t, "brad-private-session", sessionID) router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 4, func(slot int) string { if slot == 0 { return backend.server.URL } return "http://127.0.0.1:1" }) if err != nil { t.Fatal(err) } pollPath := continuityPollPath(t, router, sessionID) before := continuityPoll(t, router, subject, "__Host-hermes_chat=first-sso-session; "+tenantSessionCookie+"=webui-1", pollPath) if before.Code != http.StatusOK { t.Fatalf("first visit could not read its own session: got %d, want 200", before.Code) } // Logout invalidates the Keycloak session, so the browser returns with a // completely different oauth2-proxy cookie under the same Keycloak subject. after := continuityPoll(t, router, subject, "__Host-hermes_chat=second-sso-session; "+tenantSessionCookie+"=webui-1", pollPath) if after.Code != http.StatusOK { t.Fatalf("relogin lost the durable session: got %d, want 200", after.Code) } var payload struct { Session struct { SessionID string `json:"session_id"` Title string `json:"title"` } `json:"session"` } if err := json.NewDecoder(after.Body).Decode(&payload); err != nil { t.Fatal(err) } if payload.Session.SessionID != sessionID || payload.Session.Title != "brad-private-session" { t.Fatalf("relogin resolved the wrong session: %#v", payload.Session) } if len(backend.identities) != 2 || backend.identities[0] != "slot-0" || backend.identities[1] != "slot-0" { t.Fatalf("relogin did not keep a stable tenant identity: %#v", backend.identities) } for _, cookie := range backend.cookies { if strings.Contains(cookie, "__Host-hermes_chat") { t.Fatalf("the Keycloak session cookie crossed into a tenant: %q", cookie) } } } // Repairing continuity must not turn the poll into a cross-tenant read. func TestSessionContinuityNeverExposesAnotherSubjectsSession(t *testing.T) { const ownerSession = "owner-session-id" owner := newTenantWebUI(t, "owner-private-marker", ownerSession) intruder := newTenantWebUI(t, "intruder-private-marker") backends := []*tenantWebUI{owner, intruder} router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 2, func(slot int) string { return backends[slot].server.URL }) if err != nil { t.Fatal(err) } ownerSlot, err := router.slotFor("keycloak-owner") if err != nil { t.Fatal(err) } intruderSlot, err := router.slotFor("keycloak-intruder") if err != nil { t.Fatal(err) } if ownerSlot == intruderSlot { t.Fatal("two Keycloak subjects shared one isolated slot") } pollPath := continuityPollPath(t, router, ownerSession) if response := continuityPoll(t, router, "keycloak-owner", tenantSessionCookie+"=owner-webui", pollPath); response.Code != http.StatusOK { t.Fatalf("the owner lost its own session: got %d", response.Code) } ownerRequests := backends[ownerSlot].requests // The intruder replays the owner's session id, the owner's WebUI cookie and // a forged tenant assertion for the owner's slot. request := httptest.NewRequest(http.MethodGet, pollPath, nil) request.Header.Set("X-Forwarded-User", "keycloak-intruder") request.Header.Set("Cookie", tenantSessionCookie+"=owner-webui") request.Header.Set(trustedTenantHeader, "slot-"+strconv.Itoa(ownerSlot)) response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusNotFound { t.Fatalf("cross-subject session read got %d, want 404", response.Code) } if strings.Contains(response.Body.String(), "owner-private-marker") { t.Fatalf("another subject's session leaked: %s", response.Body.String()) } if backends[ownerSlot].requests != ownerRequests { t.Fatal("a forged tenant assertion reached another subject's backend") } identities := backends[intruderSlot].identities if len(identities) == 0 || identities[len(identities)-1] != "slot-"+strconv.Itoa(intruderSlot) { t.Fatalf("the router did not overwrite the forged tenant identity: %#v", identities) } } func TestSessionContinuityPollIsBoundedByRouter(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, _ *http.Request) { messages := make([]map[string]int, 30) for index := range messages { messages[index] = map[string]int{"index": index} } writer.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(writer).Encode(map[string]any{"session": map[string]any{ "session_id": "resolved", "messages": messages, "message_count": 30, }}) })) defer backend.Close() router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(int) string { return backend.URL }) if err != nil { t.Fatal(err) } request := httptest.NewRequest( http.MethodGet, continuityPollPath(t, router, "root"), nil, ) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("got status %d", response.Code) } payload := decodeSnapshot(t, response.Result()) if len(payload.Messages) != sessionSnapshotItems || payload.MessageCount != 30 { t.Fatalf("router returned unbounded fallback: %#v", payload) } } func TestTelegramPageExplainsOneTimeOperatorSetup(t *testing.T) { router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodGet, "/telegram", nil) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("got status %d", response.Code) } body := response.Body.String() for _, expected := range []string{ "https://t.me/BotFather", "https://secret.bstein.dev", "vault login -method=oidc", "vault kv patch -mount=kv atlas/hermes/chat-telegram", "bot_token", "relay_key", "each Keycloak user", } { if !strings.Contains(body, expected) { t.Fatalf("Telegram operator setup omitted %q", expected) } } } func TestRouterRedirectsNativeLoginToSafeChatDestination(t *testing.T) { router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } for _, test := range []struct { path string want string }{ {"/login?next=%2Fsession%2F9048e2a574d1", "/session/9048e2a574d1"}, {"/login?next=%2Fchat%3Fresume%3Dabc", "/chat?resume=abc"}, {"/login?next=https%3A%2F%2Fevil.example", "/"}, {"/login?next=%2F%2Fevil.example", "/"}, {"/login?next=%2Flogin", "/"}, {"/login?next=%2Flogin%2Fagain", "/"}, } { request := httptest.NewRequest(http.MethodGet, test.path, nil) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusFound { t.Fatalf("%s: got status %d", test.path, response.Code) } if location := response.Header().Get("Location"); location != test.want { t.Fatalf("%s: got redirect %q, want %q", test.path, location, test.want) } } } func TestRouterAllowsTenantScopedPersonalization(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNoContent) })) defer backend.Close() router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL }) if err != nil { t.Fatal(err) } for _, path := range []string{ "/api/memory", "/api/memory/write", "/api/skills", "/api/skills/save", "/api/profiles", "/api/profile/create", } { request := httptest.NewRequest(http.MethodGet, path, nil) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusNoContent { t.Fatalf("GET %s: got %d, want %d", path, response.Code, http.StatusNoContent) } } } func TestRouterAllowsPrivateWorkspaceReadsButBlocksRegistration(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { writer.WriteHeader(http.StatusNoContent) })) defer backend.Close() router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL }) if err != nil { t.Fatal(err) } for _, path := range []string{ "/api/workspaces", "/api/workspaces/suggest", "/api/file", "/api/file/raw", "/api/folder/download", "/api/logs", "/api/rollback/list", "/api/rollback/diff", } { request := httptest.NewRequest(http.MethodGet, path, nil) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusNoContent { t.Fatalf("GET %s: got %d, want %d", path, response.Code, http.StatusNoContent) } } request := httptest.NewRequest(http.MethodPost, "/api/workspaces/add", strings.NewReader(`{"path":"/opt/data"}`)) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusForbidden { t.Fatalf("workspace registration got %d, want %d", response.Code, http.StatusForbidden) } } func TestPrivateFileBrowserIsTenantAuthenticatedAndHidesKanban(t *testing.T) { router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } unauthenticated := httptest.NewRequest(http.MethodGet, "/private-files", nil) unauthenticatedResponse := httptest.NewRecorder() router.ServeHTTP(unauthenticatedResponse, unauthenticated) if unauthenticatedResponse.Code != http.StatusUnauthorized { t.Fatalf("unauthenticated browser got %d", unauthenticatedResponse.Code) } request := httptest.NewRequest(http.MethodGet, "/private-files?session_id=session-1", nil) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("got %d", response.Code) } if !strings.Contains(response.Body.String(), "Private files") || !strings.Contains(response.Body.String(), "data-files-page") { t.Fatal("private browser page was not rendered") } assetRequest := httptest.NewRequest(http.MethodGet, "/hermes-chat-bridge.js", nil) assetRequest.Header.Set("X-Forwarded-User", "subject") assetResponse := httptest.NewRecorder() router.ServeHTTP(assetResponse, assetRequest) if !strings.Contains(assetResponse.Body.String(), "endpoint('list'") || !strings.Contains(assetResponse.Body.String(), `'[data-panel="' + panel + '"]'`) { t.Fatal("file browsing or chat-only navigation policy is missing") } } func TestChatBridgeHidesOnlyNonChatCockpitChrome(t *testing.T) { router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } asset := func(path string) string { request := httptest.NewRequest(http.MethodGet, path, nil) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("%s got status %d", path, response.Code) } return response.Body.String() } js := asset("/hermes-chat-bridge.js") css := asset("/hermes-chat-bridge.css") // The whole cockpit (Kanban plus every non-chat rail panel) is declutter-hidden // both pre-JS (CSS, no flash) and by the runtime .hidden reinforcement. for _, panel := range []string{"kanban", "logs", "insights", "memory", "skills", "workspaces", "todos", "tasks", "profiles"} { if !strings.Contains(css, `[data-panel="`+panel+`"]`) { t.Fatalf("bridge CSS does not hide cockpit panel %q", panel) } if !strings.Contains(js, `'`+panel+`'`) { t.Fatalf("bridge JS does not mark cockpit panel %q hidden", panel) } } if !strings.Contains(css, "display:none") || !strings.Contains(css, "#settingsModel") { t.Fatal("bridge CSS must hide the cockpit panels and the redundant settings model selector") } // The clean chat experience (chat surface, history, composer, model chip) // must never be part of the hide set. for _, keep := range []string{`[data-panel="chat"]`, `[data-panel="history"]`, `[data-panel="sessions"]`, `[data-panel="composer"]`, "composerRoutingChip", "composerRoutingLabel"} { if strings.Contains(css, keep) || strings.Contains(js, keep) { t.Fatalf("declutter wrongly targets a core chat control: %q", keep) } } } func TestWebUIModelAndReasoningOverridesAreProxied(t *testing.T) { backend := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { if request.Method != http.MethodPost { t.Fatalf("got method %s", request.Method) } writer.WriteHeader(http.StatusNoContent) })) defer backend.Close() router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return backend.URL }) if err != nil { t.Fatal(err) } for _, path := range []string{"/api/model/set", "/api/reasoning"} { request := httptest.NewRequest(http.MethodPost, path, strings.NewReader(`{}`)) request.Header.Set("X-Forwarded-User", "subject") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusNoContent { t.Fatalf("POST %s: got %d", path, response.Code) } } } func TestTelegramLinkUsesHashedStateAndExpires(t *testing.T) { statePath := filepath.Join(t.TempDir(), "state.json") router, err := newTenantRouter(statePath, 1, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } now := time.Date(2026, 8, 8, 12, 0, 0, 0, time.UTC) router.now = func() time.Time { return now } code, expires, err := router.createLink("raw-keycloak-subject") if err != nil { t.Fatal(err) } if expires.Sub(now) != 10*time.Minute { t.Fatalf("unexpected expiry: %s", expires) } if _, err := router.consumeLink("123456789", code); err != nil { t.Fatal(err) } if _, linked := router.telegramSlot("123456789"); !linked { t.Fatal("Telegram identity was not linked") } content, err := os.ReadFile(statePath) if err != nil { t.Fatal(err) } if strings.Contains(string(content), "raw-keycloak-subject") || strings.Contains(string(content), "123456789") || strings.Contains(string(content), code) { t.Fatal("raw identity or one-time code was persisted") } secondCode, _, err := router.createLink("raw-keycloak-subject") if err != nil { t.Fatal(err) } router.now = func() time.Time { return now.Add(11 * time.Minute) } if _, err := router.consumeLink("987654321", secondCode); err == nil { t.Fatal("expected expired link code to be rejected") } } func TestTelegramWebActionsRequireExplicitSameOriginHeader(t *testing.T) { router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } router.telegram = &telegramBot{} request := httptest.NewRequest(http.MethodPost, "/api/telegram/link", strings.NewReader(`{}`)) request.Header.Set("X-Forwarded-User", "subject") request.Header.Set("Content-Type", "application/json") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusForbidden { t.Fatalf("got %d", response.Code) } router.telegram.setStatus("BsteinAtlasHermesBot", true, "") request = httptest.NewRequest(http.MethodPost, "/api/telegram/link", strings.NewReader(`{}`)) request.Header.Set("X-Forwarded-User", "subject") request.Header.Set("Content-Type", "application/json") request.Header.Set("X-Hermes-Action", "telegram-link") response = httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusOK { t.Fatalf("got %d: %s", response.Code, response.Body.String()) } } func TestTelegramWebRejectsLinkWhileConfiguredBotIsNotReady(t *testing.T) { router, err := newTenantRouter(filepath.Join(t.TempDir(), "state.json"), 1, func(slot int) string { return "" }) if err != nil { t.Fatal(err) } router.telegram = &telegramBot{} request := httptest.NewRequest(http.MethodPost, "/api/telegram/link", strings.NewReader(`{}`)) request.Header.Set("X-Forwarded-User", "subject") request.Header.Set("Content-Type", "application/json") request.Header.Set("X-Hermes-Action", "telegram-link") response := httptest.NewRecorder() router.ServeHTTP(response, request) if response.Code != http.StatusServiceUnavailable { t.Fatalf("got %d: %s", response.Code, response.Body.String()) } }