184 lines
7.8 KiB
Go
184 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.cin.su/m/Assistant/contracts/capability"
|
|
"git.cin.su/m/Assistant/contracts/interaction"
|
|
interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain"
|
|
)
|
|
|
|
func TestBearerTokenRequiresBearerScheme(t *testing.T) {
|
|
request := httptest.NewRequest("GET", "/", nil)
|
|
request.Header.Set("Authorization", "Basic value")
|
|
if token := bearerToken(request); token != "" {
|
|
t.Fatalf("unexpected token %q", token)
|
|
}
|
|
request.Header.Set("Authorization", "Bearer creator-session")
|
|
if token := bearerToken(request); token != "creator-session" {
|
|
t.Fatalf("token = %q", token)
|
|
}
|
|
}
|
|
|
|
func TestCognitiveChannelQuestionUsesHumanSafeHealthReply(t *testing.T) {
|
|
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveChannelStatus, interaction.SpeechActQuestion, capability.Health{State: capability.HealthAvailable, CapabilityID: "provider-secret-model"}, nil)
|
|
if !strings.Contains(reply, "доступен") || strings.Contains(reply, "provider-secret-model") {
|
|
t.Fatalf("unexpected safe reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestCognitiveConfigurationRequestDoesNotPretendToMutate(t *testing.T) {
|
|
reply := humanCognitiveChannelReply(interaction.InformationRequestCognitiveRecovery, interaction.SpeechActDirective, capability.Health{State: capability.HealthUnavailable}, nil)
|
|
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "аварийн") {
|
|
t.Fatalf("unexpected configuration reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestCognitiveDegradedReplyKeepsDomovoyAlive(t *testing.T) {
|
|
reply := humanCognitiveDegradedReply("Помоги настроить локальную модель")
|
|
if !strings.Contains(reply, "Домовой жив") || !strings.Contains(reply, "принято") {
|
|
t.Fatalf("unexpected degraded reply: %q", reply)
|
|
}
|
|
}
|
|
|
|
func TestModelInventoryQuestionUsesReadOnlyFastPath(t *testing.T) {
|
|
for _, message := range []string{"какие модели доступны?", "напиши список настроек ИИ", "какие есть подключения ИИ"} {
|
|
result, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: message}, nil, nil, time.Now())
|
|
if err != nil {
|
|
t.Fatalf("interpretation failed for %q: %v", message, err)
|
|
}
|
|
if result.InformationRequestKind != interaction.InformationRequestCognitiveModelInventory {
|
|
t.Fatalf("semantic inventory request %q was not recognized: kind=%q act=%q hints=%v", message, result.InformationRequestKind, result.Interpretation.SpeechAct, result.CapabilityHints)
|
|
}
|
|
}
|
|
ordinary, err := interactiondomain.InterpretUtterance(interaction.HumanUtterance{Content: "настрой локальную модель"}, nil, nil, time.Now())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if ordinary.InformationRequestKind == interaction.InformationRequestCognitiveModelInventory {
|
|
t.Fatal("configuration mutation must not use read-only model inventory path")
|
|
}
|
|
}
|
|
|
|
func TestHumanCognitiveModelInventoryDoesNotExposeSecrets(t *testing.T) {
|
|
cost := int64(25)
|
|
text := humanCognitiveModelInventory([]capability.ResolvedCognitiveBinding{{
|
|
Binding: capability.CognitiveBinding{DataPolicy: capability.DataPolicyExternalMessageOnly, Activation: capability.ActivationActive},
|
|
Provider: capability.ProviderInstance{Kind: "openai-compatible-chat-v1", CredentialRef: "secret-ref", AuthMode: capability.AuthKey, BillingMode: capability.BillingPerRequest, RequestCostMicros: &cost, Health: capability.HealthAvailable},
|
|
Model: capability.ModelDescriptor{ProviderModelID: "example-model", Health: capability.HealthAvailable},
|
|
}}, nil)
|
|
if strings.Contains(text, "secret-ref") || strings.Contains(text, "25") || !strings.Contains(text, "example-model") || !strings.Contains(text, "API-ключ") {
|
|
t.Fatalf("unsafe or incomplete inventory: %q", text)
|
|
}
|
|
}
|
|
|
|
func TestCreatorSessionHandlerAcceptsConfiguredCredentials(t *testing.T) {
|
|
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), func() time.Time { return time.Unix(1000, 0) })
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(`{"username":"m","password":"password"}`))
|
|
response := httptest.NewRecorder()
|
|
handler(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("status = %d body=%s", response.Code, response.Body.String())
|
|
}
|
|
var issued struct {
|
|
Token string
|
|
}
|
|
if err := json.NewDecoder(response.Body).Decode(&issued); err != nil || issued.Token == "" {
|
|
t.Fatalf("session response = %+v, err=%v", issued, err)
|
|
}
|
|
}
|
|
|
|
func TestCreatorSessionHandlerFailsClosed(t *testing.T) {
|
|
handler := creatorSessionHandler("recovery", "m", "password", []byte("01234567890123456789012345678901"), time.Now)
|
|
tests := []string{
|
|
`{"username":"wrong","password":"password"}`,
|
|
`{"username":"m","password":"wrong"}`,
|
|
`{"username":"","password":""}`,
|
|
`{"username":"m","password":"password","actor":"attacker"}`,
|
|
`{`,
|
|
`{"username":"m","password":"password"}{}`,
|
|
}
|
|
for _, body := range tests {
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", strings.NewReader(body))
|
|
response := httptest.NewRecorder()
|
|
handler(response, request)
|
|
if response.Code != http.StatusUnauthorized || strings.TrimSpace(response.Body.String()) != "creator authentication denied" {
|
|
t.Fatalf("body %q: status=%d response=%q", body, response.Code, response.Body.String())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCreatorSessionHandlerPreservesBootstrapRecovery(t *testing.T) {
|
|
handler := creatorSessionHandler("recovery", "", "", []byte("01234567890123456789012345678901"), time.Now)
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/creator/sessions", nil)
|
|
request.Header.Set("Authorization", "Bearer recovery")
|
|
response := httptest.NewRecorder()
|
|
handler(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("bootstrap recovery status = %d", response.Code)
|
|
}
|
|
}
|
|
|
|
func TestReadRequiredSecretFromEnvironment(t *testing.T) {
|
|
t.Setenv("TEST_CREATOR_SECRET", "direct-value")
|
|
value, err := readRequiredSecret("TEST_CREATOR_SECRET")
|
|
if err != nil || value != "direct-value" {
|
|
t.Fatalf("value=%q err=%v", value, err)
|
|
}
|
|
}
|
|
|
|
func TestReadRequiredSecretFromFile(t *testing.T) {
|
|
fileName := filepath.Join(t.TempDir(), "secret")
|
|
if err := os.WriteFile(fileName, []byte("file-value\r\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", fileName)
|
|
value, err := readRequiredSecret("TEST_CREATOR_SECRET")
|
|
if err != nil || value != "file-value" {
|
|
t.Fatalf("value=%q err=%v", value, err)
|
|
}
|
|
}
|
|
|
|
func TestReadRequiredSecretFailsClosed(t *testing.T) {
|
|
t.Run("both sources", func(t *testing.T) {
|
|
t.Setenv("TEST_CREATOR_SECRET", "direct")
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", filepath.Join(t.TempDir(), "secret"))
|
|
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
|
t.Fatal("expected ambiguous configuration to fail")
|
|
}
|
|
})
|
|
t.Run("empty file", func(t *testing.T) {
|
|
fileName := filepath.Join(t.TempDir(), "secret")
|
|
if err := os.WriteFile(fileName, nil, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", fileName)
|
|
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
|
t.Fatal("expected empty secret to fail")
|
|
}
|
|
})
|
|
t.Run("multiline file", func(t *testing.T) {
|
|
fileName := filepath.Join(t.TempDir(), "secret")
|
|
if err := os.WriteFile(fileName, []byte("first\nsecond\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", fileName)
|
|
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
|
t.Fatal("expected multiline secret to fail")
|
|
}
|
|
})
|
|
t.Run("missing file", func(t *testing.T) {
|
|
t.Setenv("TEST_CREATOR_SECRET_FILE", filepath.Join(t.TempDir(), "missing"))
|
|
if _, err := readRequiredSecret("TEST_CREATOR_SECRET"); err == nil {
|
|
t.Fatal("expected missing secret file to fail")
|
|
}
|
|
})
|
|
}
|