228 lines
12 KiB
Go
228 lines
12 KiB
Go
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)
|
|
}
|
|
}
|