Files
Assistant/cmd/core-runtime/cognitive_interpretation_test.go

261 lines
15 KiB
Go

package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"git.cin.su/m/Assistant/contracts/capability"
"git.cin.su/m/Assistant/contracts/cognitive"
"git.cin.su/m/Assistant/contracts/epistemics"
"git.cin.su/m/Assistant/contracts/identity"
"git.cin.su/m/Assistant/contracts/interaction"
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
)
func TestReviewResponseUsesCognitiveGatewayAndReturnsOnlyDraftAnswer(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode review request: %v", err)
}
if request.Message == "" || request.Meaning.SpeechAct == "" {
t.Fatalf("review request did not carry semantic context: %+v", request)
}
if !strings.Contains(request.Message, "ответ начинается с содержания") || !strings.Contains(request.Message, "Не повторяй вопрос пользователя") || !strings.Contains(request.Message, "только если пользователь сам спросил") {
t.Fatalf("review request lacks the conversational quality contract: %q", request.Message)
}
if len(request.Context) != 1 || !strings.Contains(request.Context[0].Text, "красный цвет") {
t.Fatalf("review request did not carry compact personal memory: %+v", request.Context)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Меня зовут Дед. Я Домовой.", FinishReason: cognitive.FinishCompleted})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
reviewed, err := client.ReviewResponse(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "кто ты?", "Я — Домовой.", "Предпочтение пользователя (его сообщение): красный цвет", cognitive.TaskMeaning{SpeechAct: "QUESTION"})
if err != nil {
t.Fatal(err)
}
if reviewed != "Меня зовут Дед. Я Домовой." {
t.Fatalf("unexpected reviewed response %q", reviewed)
}
}
func TestAnswerStableKnowledgeUsesLocalOnlyCognitiveRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatalf("decode stable knowledge request: %v", err)
}
if request.DataPolicy != capability.DataPolicyLocalOnly || request.Meaning.SpeechAct != "STABLE_KNOWLEDGE_ANSWER" {
t.Fatalf("unexpected stable knowledge request: %+v", request)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "Санкт-Петербург — город в России.", FinishReason: cognitive.FinishCompleted})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
answer, err := client.AnswerStableKnowledge(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "Питер — это где?", "", cognitive.TaskMeaning{SpeechAct: "QUESTION"})
if err != nil || answer != "Санкт-Петербург — город в России." {
t.Fatalf("answer=%q err=%v", answer, err)
}
}
func TestCompileInteractionContextAppliesTypedScopeBeforeGateway(t *testing.T) {
context, err := compileInteractionContext(interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{
{UtteranceID: "a", Text: "контрольный код = ALPHA", ScopeRef: "company-a", Relevance: 2},
{UtteranceID: "b", Text: "контрольный код = BETA", ScopeRef: "company-b", Relevance: 3},
}}, "company-a")
if err != nil || context != "контрольный код = ALPHA" {
t.Fatalf("context=%q err=%v", context, err)
}
}
func TestCompileInteractionContextUsesHistoricalOnlyWhenRequested(t *testing.T) {
projection := interactiondomain.ContextProjection{Fragments: []interactiondomain.ContextFragment{{UtteranceID: "current", Text: "код = NEW", Relevance: 2}}, Historical: []interactiondomain.ContextFragment{{UtteranceID: "old", Text: "код = OLD", Relevance: 1, Historical: true}}}
current, err := compileInteractionContextWithExperience(projection, "", nil, "Какой текущий код?")
if err != nil || strings.Contains(current, "OLD") {
t.Fatalf("current context=%q err=%v", current, err)
}
historical, err := compileInteractionContextWithExperience(projection, "", nil, "Какой старый код был раньше?")
if err != nil || !strings.Contains(historical, "OLD") {
t.Fatalf("historical context=%q err=%v", historical, err)
}
}
func TestStableKnowledgeContinuesOnceWithBoundedAdditionalContext(t *testing.T) {
calls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
var request cognitive.GatewayAdvisoryRequest
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
t.Fatal(err)
}
if calls == 1 {
if !request.AllowIterativeRetrieval {
t.Fatal("initial advisory did not permit bounded continuation")
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted, IterativeRetrieval: &cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "initial context is insufficient", RemainingBytes: 512}})
return
}
if request.AllowIterativeRetrieval || len(request.Context) != 1 || request.Context[0].Text != "контрольный код = ORBIT-731" {
t.Fatalf("continuation request=%+v", request)
}
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted, AdvisoryText: "Контрольный код — ORBIT-731."})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
answer, err := client.AnswerStableKnowledgeWithContinuation(context.Background(), identity.CreatorSession{ActorID: "actor", PrincipalID: "principal"}, "Какой контрольный код?", "", cognitive.TaskMeaning{SpeechAct: "QUESTION"}, func(context.Context, cognitive.IterativeRetrievalRequest) cognitive.IterativeRetrievalResponse {
return cognitive.IterativeRetrievalResponse{Candidates: []cognitive.ContextCandidate{{Excerpt: cognitive.ContextExcerpt{ID: "interaction:code", Text: "контрольный код = ORBIT-731", Sensitivity: cognitive.SensitivityPrivateAllowed}, Lane: "current-fact"}}, Exhausted: true, AddedBytes: 28}
})
if err != nil || answer != "Контрольный код — ORBIT-731." || calls != 2 {
t.Fatalf("answer=%q err=%v calls=%d", answer, err, calls)
}
}
func TestIterativeRetrieverPreservesScopeLanesAndBudgets(t *testing.T) {
history := []interaction.HumanUtterance{{ID: "a", PrincipalID: "p", Content: "контрольный код = ALPHA", ReceivedAtUnixMillis: 1}, {ID: "b", PrincipalID: "p", Content: "контрольный код = BETA", ReceivedAtUnixMillis: 2}}
references := []interaction.ProjectReference{{ID: "a-ref", UtteranceID: "a", ProjectID: "company-a"}, {ID: "b-ref", UtteranceID: "b", ProjectID: "company-b"}}
experiences := []epistemics.ExperienceRecord{{ID: "failure", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "catalog sync", ActualOutcome: epistemics.OutcomeFailed, OutcomeDetail: "old connector failed", RecordedAtUnixMillis: 1}, {ID: "conflict", PrincipalID: "p", ContextScopeRef: "company-a", ProblemStatement: "catalog sync", ActualOutcome: epistemics.OutcomeConflict, OutcomeDetail: "conflicting result", RecordedAtUnixMillis: 2}}
retrieve := newIterativeContextRetriever(history, references, "p", "company-a", experiences)
current := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "need scoped fact", RemainingBytes: 512})
if len(current.Candidates) != 1 || !strings.Contains(current.Candidates[0].Excerpt.Text, "ALPHA") || strings.Contains(current.Candidates[0].Excerpt.Text, "BETA") {
t.Fatalf("scope leaked: %#v", current)
}
if duplicate := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "контрольный код", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "repeat", RemainingBytes: 512}); duplicate.Reason != "no_additional_evidence" {
t.Fatalf("duplicate was re-added: %#v", duplicate)
}
if negative := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "catalog sync", Lane: cognitive.IterativeRetrievalNegativeExperience, Reason: "need failure", RemainingBytes: 512}); len(negative.Candidates) < 1 || !strings.Contains(strings.Join([]string{negative.Candidates[0].Excerpt.Text, negative.Candidates[len(negative.Candidates)-1].Excerpt.Text}, " "), "old connector failed") {
t.Fatalf("negative lane=%#v", negative)
}
if contradiction := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "catalog sync", Lane: cognitive.IterativeRetrievalContradiction, Reason: "need conflict", RemainingBytes: 512}); len(contradiction.Candidates) != 1 || !strings.Contains(contradiction.Candidates[0].Excerpt.Text, "conflicting result") {
t.Fatalf("contradiction lane=%#v", contradiction)
}
if budget := retrieve(context.Background(), cognitive.IterativeRetrievalRequest{Query: "anything", Lane: cognitive.IterativeRetrievalCurrentFact, Reason: "bad", RemainingBytes: 2048}); budget.Reason != "budget" {
t.Fatalf("budget=%#v", budget)
}
}
func TestGenerateImageUsesDedicatedCognitiveMediaRoute(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || r.URL.Path != "/v1/multimodal/generate-image" {
t.Fatalf("unexpected image route: %s %s", r.Method, r.URL.Path)
}
if err := r.ParseMultipartForm(1 << 20); err != nil {
t.Fatalf("parse image request: %v", err)
}
if !strings.Contains(r.FormValue("prompt"), "собаку") {
t.Fatalf("prompt was not preserved: %q", r.FormValue("prompt"))
}
_ = json.NewEncoder(w).Encode(capability.MultimodalResponse{Operation: capability.MultimodalGenerateImage, ContentType: "image/png", DataBase64: "cG5n"})
}))
defer server.Close()
client, err := newCognitiveInterpretationClient(server.URL, server.Client())
if err != nil {
t.Fatal(err)
}
image, err := client.GenerateImage(context.Background(), "нарисуй собаку", "")
if err != nil || image.DataBase64 != "cG5n" || image.ContentType != "image/png" {
t.Fatalf("image=%+v err=%v", image, err)
}
}
func TestReviewPreservesResearchEvidence(t *testing.T) {
draft := "Нашёл вариант. Источник: Aviasales (https://www.aviasales.ru/search)"
if reviewPreservesResearchEvidence("Нашёл вариант на Авиасейлс.", draft) {
t.Fatal("review without the direct source must be rejected")
}
if !reviewPreservesResearchEvidence("Нашёл вариант. Источник: Aviasales (https://www.aviasales.ru/search)", draft) {
t.Fatal("review with the direct source must be accepted")
}
}
func TestSimpleQuestionUsesStrictInterpretationForSemanticRouting(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActQuestion,
Applicability: interaction.SemanticNeedsBroaderInterpret,
Clarification: interaction.ClarificationNotRequired,
},
CapabilityHints: []string{"COGNITIVE_REASONING", "VERIFY_FACT"},
}
if !shouldRequestBroaderInterpretation(result) {
t.Fatal("simple question must reach semantic interpretation before any answer")
}
}
func TestOperationalQuestionStillUsesStrictInterpretation(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActQuestion,
Applicability: interaction.SemanticNeedsBroaderInterpret,
Clarification: interaction.ClarificationNotRequired,
},
CapabilityHints: []string{"MODIFY_CODE", "RUN_TESTS"},
}
if !shouldRequestBroaderInterpretation(result) {
t.Fatal("operational question must retain strict interpretation routing")
}
}
func TestStructuredInventoryUsesDirectReadOnlySemanticRoute(t *testing.T) {
result := interactiondomain.SemanticResult{
Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActDirective,
Applicability: interaction.SemanticApplicable,
ConfidenceMillis: 980,
},
InformationRequestKind: interaction.InformationRequestCognitiveModelInventory,
}
if shouldRequestSemanticInterpretation(result) {
t.Fatal("structured model inventory must use the direct read-only semantic route")
}
}
func TestStableReflexIsPeriodicallySampledForDrift(t *testing.T) {
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActConversational, Applicability: interaction.SemanticApplicable,
}}
found := false
for minute := 0; minute < 160; minute++ {
if shouldAuditDeterministicReflex(result, "utterance-drift-probe", time.Unix(int64(minute)*60, 0)) {
found = true
break
}
}
if !found {
t.Fatal("stable reflex was never sampled during the audit window")
}
}
func TestMeaningfulSignalsAreNotReflexOnly(t *testing.T) {
for _, act := range []interaction.SpeechAct{interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate, interaction.SpeechActDirective} {
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{SpeechAct: act, Applicability: interaction.SemanticApplicable}}
if !shouldRequestSemanticInterpretation(result) {
t.Fatalf("%s must prefer cognitive interpretation", act)
}
}
}
func TestExplicitPreferenceIsNotReinterpretedByCognitiveAudit(t *testing.T) {
result := interactiondomain.SemanticResult{Interpretation: interaction.UtteranceInterpretation{
SpeechAct: interaction.SpeechActPreference, Applicability: interaction.SemanticApplicable,
}}
if shouldRequestSemanticInterpretation(result) || shouldAuditDeterministicReflex(result, "preference", time.Unix(0, 0)) {
t.Fatal("explicit preference must remain on the deterministic durable-context path")
}
}