package main import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" "git.cin.su/m/Assistant/contracts/capability" "git.cin.su/m/Assistant/contracts/cognitive" "git.cin.su/m/Assistant/contracts/continuity" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" ) func TestChatPageServedWithoutInternalIDs(t *testing.T) { t.Setenv("ASSISTANT_BUILD_REVISION", "0123456789abcdef") t.Setenv("ASSISTANT_BUILD_TIME", "2026-08-22T03:04:05Z") handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive") if err != nil { t.Fatal(err) } recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, "/chat", nil)) if recorder.Code != http.StatusOK { t.Fatalf("expected /chat status 200, got %d", recorder.Code) } body := recorder.Body.String() for _, required := range []string{"cognitive-status", "aria-live=\"polite\"", "cognitive-console-link", "href=\"/admin\"", "Проверь канал", "Изменить настройку", "ai-data-error", "Не вставляйте пароли", "build-meta", "версия ", "обновлено ", "datetime=\""} { if !strings.Contains(body, required) { t.Fatalf("/chat is missing human status marker %q", required) } } for _, forbidden := range []string{"provider_id", "model_id", "binding_id", "workspace-id", "session-id", "__ASSISTANT_BUILD_REVISION__", "__ASSISTANT_BUILD_TIME__", "__ASSISTANT_BUILD_DATETIME__"} { if strings.Contains(body, forbidden) { t.Fatalf("/chat leaks internal field %q", forbidden) } } if !strings.Contains(body, "версия 0123456789ab") || !strings.Contains(body, "обновлено 22.08.2026 03:04 UTC") || !strings.Contains(body, "datetime=\"2026-08-22T03:04:05Z\"") { t.Fatalf("/chat does not render build metadata: %q", body) } for _, path := range []string{"/", "/chat.html"} { alternate := httptest.NewRecorder() handler.ServeHTTP(alternate, httptest.NewRequest(http.MethodGet, path, nil)) if alternate.Code != http.StatusOK || strings.Contains(alternate.Body.String(), "__ASSISTANT_BUILD_REVISION__") { t.Fatalf("%s did not render build metadata: status=%d", path, alternate.Code) } } } func TestChatScriptExplainsSessionAndRuntimeFailures(t *testing.T) { body, err := staticFiles.ReadFile("static/assets/chat.js") if err != nil { t.Fatal(err) } script := string(body) for _, required := range []string{"class ChatApiError", "Сессия устарела", "основной runtime сейчас недоступен", "Проверьте текст и повторите попытку"} { if !strings.Contains(script, required) { t.Fatalf("chat script is missing recovery message %q", required) } } } func TestPublicSurfacesSeparateChatAndConsole(t *testing.T) { handler, err := newHandler("http://core", "http://continuity", "http://commit", http.DefaultClient, "http://cognitive") if err != nil { t.Fatal(err) } for _, test := range []struct { path string must string mustNot string }{ {path: "/", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"}, {path: "/admin", must: "Чат с Домовым", mustNot: "Напишите Домовому обычное сообщение"}, {path: "/chat", must: "Напишите Домовому обычное сообщение", mustNot: "context-preset"}, } { recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, httptest.NewRequest(http.MethodGet, test.path, nil)) body := recorder.Body.String() if recorder.Code != http.StatusOK || !strings.Contains(body, test.must) || strings.Contains(body, test.mustNot) { t.Fatalf("%s response=%d contains expected=%t forbidden=%t", test.path, recorder.Code, strings.Contains(body, test.must), strings.Contains(body, test.mustNot)) } } } func TestChatMessageUsesCoreAndLocalOnlyCognitiveGateway(t *testing.T) { var cognitiveRequest cognitive.GatewayAdvisoryRequest core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current": _ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"}) case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages": var request interaction.ChatMessageRequest if err := json.NewDecoder(r.Body).Decode(&request); err != nil { t.Fatal(err) } if request.Message != "Кто ты?" || request.ConversationID != "" { t.Fatalf("unexpected core chat request: %#v", request) } w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, InterpretationState: interaction.InterpretationInterpreted, NeedsCognition: true, FallbackText: "fallback", InformationRequestKind: interaction.InformationRequestCognitiveChannelStatus}) default: t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path) } })) defer core.Close() continuity := httptest.NewServer(http.NotFoundHandler()) defer continuity.Close() commit := httptest.NewServer(http.NotFoundHandler()) defer commit.Close() cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost || r.URL.Path != "/v1/advisories" { t.Fatalf("unexpected cognitive route %s %s", r.Method, r.URL.Path) } if err := json.NewDecoder(r.Body).Decode(&cognitiveRequest); err != nil { t.Fatal(err) } _ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: cognitiveRequest.RequestID, AdvisoryText: "Я Домовой.", FinishReason: cognitive.FinishCompleted, ReferencedExcerptIDs: []cognitive.ContextExcerptID{}}) })) defer cognitiveRuntime.Close() handler, err := newHandler(core.URL, continuity.URL, commit.URL, core.Client(), cognitiveRuntime.URL) if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Кто ты?"}`)) request.Header.Set("Authorization", "Bearer session-token") request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("expected status 200, got %d body=%s", recorder.Code, recorder.Body.String()) } var response browserChatResponse if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatal(err) } if response.ReplyText != "Я Домовой." || response.ConversationID != "conversation-1" || response.SpeechAct != string(interaction.SpeechActQuestion) { t.Fatalf("unexpected chat response: %#v", response) } if cognitiveRequest.DataPolicy != capability.DataPolicyLocalOnly || cognitiveRequest.EgressAcknowledged { t.Fatalf("unexpected cognitive policy: %#v", cognitiveRequest) } if cognitiveRequest.ActorID != "actor-1" || cognitiveRequest.PrincipalID != "principal-1" || cognitiveRequest.ScopeID != "domovoy-global-scope" || cognitiveRequest.Message != "Кто ты?" { t.Fatalf("unexpected trusted gateway request: %#v", cognitiveRequest) } if cognitiveRequest.Meaning.InformationRequestKind != interaction.InformationRequestCognitiveChannelStatus { t.Fatalf("information intent was not preserved: %#v", cognitiveRequest.Meaning) } } func TestChatMessageReturnsHumanCapabilityMetadata(t *testing.T) { core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current": _ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"}) case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages": w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActDirective, FallbackText: "fallback", PlanningReply: "Готово. Я создал напоминание: позвонить врачу.", CapabilityStatus: capability.ResolutionAvailable, CapabilityKind: capability.CapabilityKindScheduleReminder}) default: t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path) } })) defer core.Close() handler, err := newHandler(core.URL, "http://continuity", "http://commit", core.Client(), "http://cognitive") if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Напомни завтра в 10 позвонить врачу."}`)) request.Header.Set("Authorization", "Bearer session-token") request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } var response browserChatResponse if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatal(err) } if response.ReplyText != "Готово. Я создал напоминание: позвонить врачу." || response.CapabilityStatus != string(capability.ResolutionAvailable) || response.CapabilityKind != string(capability.CapabilityKindScheduleReminder) { t.Fatalf("unexpected browser response: %#v", response) } } func TestChatAttentionQuestionUsesContinuityToday(t *testing.T) { core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.Method == http.MethodGet && r.URL.Path == "/v1/creator/sessions/current": _ = json.NewEncoder(w).Encode(identity.CreatorSession{ActorID: "actor-1", PrincipalID: "principal-1"}) case r.Method == http.MethodPost && r.URL.Path == "/v1/creator/chat/messages": w.WriteHeader(http.StatusCreated) _ = json.NewEncoder(w).Encode(interaction.ChatMessageResponse{ConversationID: "conversation-1", UtteranceID: "utterance-1", SpeechAct: interaction.SpeechActQuestion, FallbackText: "Сейчас в фокусе: УПО."}) default: t.Fatalf("unexpected core route %s %s", r.Method, r.URL.Path) } })) defer core.Close() continuityRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet || r.URL.Path != "/v1/creator/today" { t.Fatalf("unexpected continuity route %s %s", r.Method, r.URL.Path) } _ = json.NewEncoder(w).Encode(continuity.TodayResult{Items: []continuity.TodayItem{{Title: "проверить уведомление", HumanState: "доставлено", DeliveryState: "уведомление отправлено"}}}) })) defer continuityRuntime.Close() handler, err := newHandler(core.URL, continuityRuntime.URL, "http://commit", core.Client(), "http://cognitive") if err != nil { t.Fatal(err) } request := httptest.NewRequest(http.MethodPost, "/api/chat/messages", strings.NewReader(`{"message":"Что сейчас требует моего внимания?"}`)) request.Header.Set("Authorization", "Bearer session-token") request.Header.Set("Content-Type", "application/json") recorder := httptest.NewRecorder() handler.ServeHTTP(recorder, request) if recorder.Code != http.StatusOK { t.Fatalf("status=%d body=%s", recorder.Code, recorder.Body.String()) } var response browserChatResponse if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil { t.Fatal(err) } if !strings.Contains(response.ReplyText, "проверить уведомление") || strings.Contains(response.ReplyText, "УПО") { t.Fatalf("attention reply did not use canonical today: %#v", response) } }