583 lines
29 KiB
Go
583 lines
29 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.cin.su/m/Assistant/contracts/cognitive"
|
|
"git.cin.su/m/Assistant/contracts/interaction"
|
|
)
|
|
|
|
func TestLocalChatContextUsesOnlyActiveProject(t *testing.T) {
|
|
context := localChatContext(interaction.ChatMessageResponse{ActiveProjectName: "Домовой"})
|
|
if len(context) != 1 || context[0].SourceKind != "situation-active-project" || context[0].Text != "Текущий проект: Домовой" {
|
|
t.Fatalf("unexpected local context: %#v", context)
|
|
}
|
|
if empty := localChatContext(interaction.ChatMessageResponse{}); len(empty) != 0 {
|
|
t.Fatalf("expected empty context without active project: %#v", empty)
|
|
}
|
|
}
|
|
|
|
func TestCreatorRouteAllowsOnlyBoundedDelegationReads(t *testing.T) {
|
|
tests := []struct {
|
|
path string
|
|
want string
|
|
}{
|
|
{"/api/delegations/delegation-1", "/v1/creator/delegations/delegation-1"},
|
|
{"/api/delegations/delegation-1/latest-checkpoint", "/v1/creator/delegations/delegation-1/latest-checkpoint"},
|
|
}
|
|
for _, test := range tests {
|
|
_, got, ok := creatorRoute(http.MethodGet, test.path)
|
|
if !ok || got != test.want {
|
|
t.Fatalf("creatorRoute(%q) = %q, %t; want %q, true", test.path, got, ok, test.want)
|
|
}
|
|
}
|
|
if _, _, ok := creatorRoute(http.MethodGet, "/api/delegations/../secrets"); ok {
|
|
t.Fatal("path traversal-like delegation route must be denied")
|
|
}
|
|
}
|
|
|
|
func TestAgentWorkerStatusProxyRequiresCreatorSession(t *testing.T) {
|
|
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer bounded-session" {
|
|
t.Fatalf("unexpected session request path=%s auth=%q", r.URL.Path, r.Header.Get("Authorization"))
|
|
}
|
|
_, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`)
|
|
}))
|
|
defer core.Close()
|
|
worker := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/status" {
|
|
t.Fatalf("unexpected worker path %s", r.URL.Path)
|
|
}
|
|
_, _ = io.WriteString(w, `{"runtime":"agent-worker","executor_configured":false,"mode":"recovery-only","ready":false,"reason":"bridge missing"}`)
|
|
}))
|
|
defer worker.Close()
|
|
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), "http://cognitive.invalid", worker.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
denied := httptest.NewRecorder()
|
|
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodGet, "/api/agent-worker/status", nil))
|
|
if denied.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unauthenticated status=%d", denied.Code)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/api/agent-worker/status", nil)
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"mode":"recovery-only"`) || response.Header().Get("Cache-Control") != "no-store" {
|
|
t.Fatalf("response=%d cache=%q body=%s", response.Code, response.Header().Get("Cache-Control"), response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestFleetReadinessRequiresCreatorSessionAndProxiesCognitiveState(t *testing.T) {
|
|
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer bounded-session" {
|
|
t.Fatalf("unexpected session request path=%s auth=%q", r.URL.Path, r.Header.Get("Authorization"))
|
|
}
|
|
_, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`)
|
|
}))
|
|
defer core.Close()
|
|
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/fleet/readiness" {
|
|
t.Fatalf("unexpected cognitive path %s", r.URL.Path)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"readiness":{"members":[{"provider_instance_id":"lifeboat","model_descriptor_id":"qwen","tier":"LIFEBOAT","ready":true,"reason":"AVAILABLE"}],"ready_tiers":["LIFEBOAT"],"fallback_tier":"LIFEBOAT"},"preferred_tier":"HEAVY","selected_tier":"LIFEBOAT"}`)
|
|
}))
|
|
defer cognitiveRuntime.Close()
|
|
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), cognitiveRuntime.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
denied := httptest.NewRecorder()
|
|
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodGet, "/api/fleet/readiness", nil))
|
|
if denied.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unauthenticated status=%d", denied.Code)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/api/fleet/readiness", nil)
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), `"selected_tier":"LIFEBOAT"`) || response.Header().Get("Cache-Control") != "no-store" {
|
|
t.Fatalf("response=%d cache=%q body=%s", response.Code, response.Header().Get("Cache-Control"), response.Body.String())
|
|
}
|
|
}
|
|
|
|
const legacyExternalMode = "legacy-external-test-mode"
|
|
|
|
func newHandlerWithAdvisoryMode(coreURL, continuityURL, commitURL string, client *http.Client, mode string, cognitiveURLs ...string) (http.Handler, error) {
|
|
if mode != legacyExternalMode {
|
|
return nil, fmt.Errorf("invalid advisory binding mode %q", mode)
|
|
}
|
|
return newHandler(coreURL, continuityURL, commitURL, client, cognitiveURLs...)
|
|
}
|
|
|
|
func TestGatewayForwardsOnlyCreatorAllowlist(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/tasks/task-1" || r.URL.RawQuery != "session_id=session-1&workspace_id=workspace-1" {
|
|
t.Fatalf("unexpected upstream target %s?%s", r.URL.Path, r.URL.RawQuery)
|
|
}
|
|
if r.Header.Get("Authorization") != "Bearer bounded-session" {
|
|
t.Fatal("authorization header was not forwarded")
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = io.WriteString(w, `{"TaskID":"task-1"}`)
|
|
}))
|
|
defer upstream.Close()
|
|
handler, err := newHandler(upstream.URL, upstream.URL, upstream.URL, upstream.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/api/tasks/task-1?session_id=session-1&workspace_id=workspace-1", nil)
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "task-1") {
|
|
t.Fatalf("response = %d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGatewayForwardsCreatorCredentialsOnlyInBody(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/sessions" || r.Method != http.MethodPost {
|
|
t.Fatalf("unexpected session target %s %s", r.Method, r.URL.Path)
|
|
}
|
|
if r.Header.Get("Authorization") != "" || r.Header.Get("Content-Type") != "application/json" {
|
|
t.Fatalf("unexpected session headers Authorization=%q Content-Type=%q", r.Header.Get("Authorization"), r.Header.Get("Content-Type"))
|
|
}
|
|
body, _ := io.ReadAll(r.Body)
|
|
if string(body) != `{"username":"m","password":"secret"}` {
|
|
t.Fatalf("session body = %s", body)
|
|
}
|
|
_, _ = io.WriteString(w, `{"Token":"bounded"}`)
|
|
}))
|
|
defer upstream.Close()
|
|
handler, err := newHandler(upstream.URL, upstream.URL, upstream.URL, upstream.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/session", strings.NewReader(`{"username":"m","password":"secret"}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("session response = %d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGatewayDeniesInternalAndMutationRoutes(t *testing.T) {
|
|
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, test := range []struct{ method, path string }{
|
|
{http.MethodPost, "/api/tasks/task-1"},
|
|
{http.MethodGet, "/api/principal-bindings/actor/principal"},
|
|
{http.MethodPost, "/api/focus-task-commits"},
|
|
{http.MethodPost, "/api/prospective-items/item-1"},
|
|
{http.MethodGet, "/api/tasks/../../healthz"},
|
|
{http.MethodPost, "/api/actions/arbitrary"},
|
|
{http.MethodGet, "/api/actions/satisfy-delivered"},
|
|
{http.MethodGet, "/api/actions/notify-delivered"},
|
|
} {
|
|
request := httptest.NewRequest(test.method, test.path, nil)
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusNotFound {
|
|
t.Fatalf("%s %s returned %d", test.method, test.path, response.Code)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestGatewayRoutesProspectiveItemToContinuity(t *testing.T) {
|
|
continuity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/prospective-items/item-1" {
|
|
t.Fatalf("unexpected Continuity target %s", r.URL.Path)
|
|
}
|
|
if r.Header.Get("Authorization") != "Bearer bounded-session" {
|
|
t.Fatal("authorization header was not forwarded")
|
|
}
|
|
_, _ = io.WriteString(w, `{"ID":"item-1"}`)
|
|
}))
|
|
defer continuity.Close()
|
|
handler, err := newHandler("http://core-runtime.invalid", continuity.URL, "http://commit-runtime.invalid", continuity.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/api/prospective-items/item-1", nil)
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "item-1") {
|
|
t.Fatalf("response = %d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGatewayRoutesProspectiveListToContinuity(t *testing.T) {
|
|
continuity := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/prospective-items" || r.URL.RawQuery != "limit=25" {
|
|
t.Fatalf("unexpected Continuity list target %s?%s", r.URL.Path, r.URL.RawQuery)
|
|
}
|
|
_, _ = io.WriteString(w, `{"Items":[]}`)
|
|
}))
|
|
defer continuity.Close()
|
|
handler, err := newHandler("http://core-runtime.invalid", continuity.URL, "http://commit-runtime.invalid", continuity.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/api/prospective-items?limit=25", nil)
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Items") {
|
|
t.Fatalf("response = %d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGatewayRoutesOnlyBoundedCreatorCommandsToCommit(t *testing.T) {
|
|
commitRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/continuity/satisfy-delivered" || r.Method != http.MethodPost {
|
|
t.Fatalf("unexpected Commit target %s %s", r.Method, r.URL.Path)
|
|
}
|
|
if r.Header.Get("Authorization") != "Bearer bounded-session" {
|
|
t.Fatal("authorization header was not forwarded")
|
|
}
|
|
body, _ := io.ReadAll(r.Body)
|
|
if !strings.Contains(string(body), "prospective-1") || strings.Contains(string(body), "GrantID") || strings.Contains(string(body), "CapabilityID") {
|
|
t.Fatalf("unexpected command body %s", body)
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
_, _ = io.WriteString(w, `{"State":"SUCCEEDED"}`)
|
|
}))
|
|
defer commitRuntime.Close()
|
|
handler, err := newHandler("http://core-runtime.invalid", "http://continuity-runtime.invalid", commitRuntime.URL, commitRuntime.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/actions/satisfy-delivered", strings.NewReader(`{"ProspectiveItemID":"prospective-1"}`))
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusCreated || !strings.Contains(response.Body.String(), "SUCCEEDED") {
|
|
t.Fatalf("response = %d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGatewayRoutesTypedNotificationCommandToCommit(t *testing.T) {
|
|
commitRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/continuity/notify-delivered" || r.Method != http.MethodPost {
|
|
t.Fatalf("unexpected Commit target %s %s", r.Method, r.URL.Path)
|
|
}
|
|
body, _ := io.ReadAll(r.Body)
|
|
text := string(body)
|
|
if !strings.Contains(text, "prospective-1") || !strings.Contains(text, "Confirm") || strings.Contains(text, "URL") || strings.Contains(text, "CapabilityID") {
|
|
t.Fatalf("unexpected notification body %s", text)
|
|
}
|
|
w.WriteHeader(http.StatusCreated)
|
|
_, _ = io.WriteString(w, `{"State":"SUCCEEDED"}`)
|
|
}))
|
|
defer commitRuntime.Close()
|
|
handler, err := newHandler("http://core-runtime.invalid", "http://continuity-runtime.invalid", commitRuntime.URL, commitRuntime.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/actions/notify-delivered", strings.NewReader(`{"ProspectiveItemID":"prospective-1","Confirm":true}`))
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusCreated {
|
|
t.Fatalf("response=%d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestGatewayRoutesCreatorActionHistoryToCommit(t *testing.T) {
|
|
commitRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/authorized-actions" || r.URL.RawQuery != "limit=25" || r.Method != http.MethodGet {
|
|
t.Fatalf("unexpected Commit history target %s?%s %s", r.URL.Path, r.URL.RawQuery, r.Method)
|
|
}
|
|
if r.Header.Get("Authorization") != "Bearer bounded-session" {
|
|
t.Fatal("authorization header was not forwarded")
|
|
}
|
|
_, _ = io.WriteString(w, `{"Items":[{"AuthorizedActionID":"authorized-1"}]}`)
|
|
}))
|
|
defer commitRuntime.Close()
|
|
handler, err := newHandler("http://core-runtime.invalid", "http://continuity-runtime.invalid", commitRuntime.URL, commitRuntime.Client())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/api/actions?limit=25", nil)
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "authorized-1") {
|
|
t.Fatalf("response = %d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestConsoleAndSecurityHeaders(t *testing.T) {
|
|
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/admin", nil)
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Домовой") ||
|
|
!strings.Contains(response.Body.String(), "context-preset") ||
|
|
!strings.Contains(response.Body.String(), "context-state") ||
|
|
!strings.Contains(response.Body.String(), "copy-context") ||
|
|
!strings.Contains(response.Body.String(), "К содержимому") ||
|
|
!strings.Contains(response.Body.String(), "aria-current=\"location\"") ||
|
|
!strings.Contains(response.Body.String(), "section-nav-link") ||
|
|
!strings.Contains(response.Body.String(), "data-page=\"overview\"") ||
|
|
!strings.Contains(response.Body.String(), "data-page=\"diagnostics\"") ||
|
|
!strings.Contains(response.Body.String(), "data-page=\"operations\"") ||
|
|
!strings.Contains(response.Body.String(), "data-page=\"learning\"") ||
|
|
!strings.Contains(response.Body.String(), "data-page=\"settings\"") ||
|
|
!strings.Contains(response.Body.String(), "satisfaction-dialog") || !strings.Contains(response.Body.String(), "Подтверждаю переход") ||
|
|
!strings.Contains(response.Body.String(), "action-history") || !strings.Contains(response.Body.String(), "Журнал действий") ||
|
|
!strings.Contains(response.Body.String(), "advisory-form") || !strings.Contains(response.Body.String(), "Спросить Домового") ||
|
|
!strings.Contains(response.Body.String(), "advisory-egress-ack") || !strings.Contains(response.Body.String(), "внешнему провайдеру") ||
|
|
!strings.Contains(response.Body.String(), "creator-username") || !strings.Contains(response.Body.String(), "creator-password") ||
|
|
strings.Contains(response.Body.String(), "bootstrap-token") {
|
|
t.Fatalf("console response = %d", response.Code)
|
|
}
|
|
if response.Header().Get("Content-Security-Policy") == "" {
|
|
t.Fatal("content security policy is missing")
|
|
}
|
|
}
|
|
|
|
func TestLoginRouteServesChatShell(t *testing.T) {
|
|
handler, err := newHandler("http://core-runtime:8080", "http://continuity-runtime:8081", "http://commit-runtime:8082", http.DefaultClient)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, httptest.NewRequest(http.MethodGet, "/login", nil))
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "id=\"username\"") || !strings.Contains(response.Body.String(), "id=\"password\"") {
|
|
t.Fatalf("login response = %d", response.Code)
|
|
}
|
|
}
|
|
|
|
func TestAdvisoryModeRejectsInvalidConfiguration(t *testing.T) {
|
|
_, err := newHandlerWithAdvisoryMode("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, "browser-selected")
|
|
if err == nil || !strings.Contains(err.Error(), "invalid advisory binding mode") {
|
|
t.Fatalf("error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestLegacyExternalAdvisoryPolicyRequiresAuthenticatedCreatorSession(t *testing.T) {
|
|
t.Skip("superseded by registry-derived advisory policy tests")
|
|
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/creator/sessions/current" || r.Header.Get("Authorization") != "Bearer bounded-session" {
|
|
t.Fatalf("unexpected policy authentication request %s %q", r.URL.Path, r.Header.Get("Authorization"))
|
|
}
|
|
_, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`)
|
|
}))
|
|
defer core.Close()
|
|
handler, err := newHandlerWithAdvisoryMode(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), legacyExternalMode)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
denied := httptest.NewRecorder()
|
|
handler.ServeHTTP(denied, httptest.NewRequest(http.MethodGet, "/api/advisory-policy", nil))
|
|
if denied.Code != http.StatusUnauthorized {
|
|
t.Fatalf("unauthenticated status=%d", denied.Code)
|
|
}
|
|
request := httptest.NewRequest(http.MethodGet, "/api/advisory-policy", nil)
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || response.Header().Get("Cache-Control") != "no-store" {
|
|
t.Fatalf("response=%d cache=%q body=%s", response.Code, response.Header().Get("Cache-Control"), response.Body.String())
|
|
}
|
|
var policy advisoryPolicy
|
|
if err := json.Unmarshal(response.Body.Bytes(), &policy); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !policy.ExternalEgress || !policy.AcknowledgementRequired {
|
|
t.Fatalf("unexpected policy %+v", policy)
|
|
}
|
|
}
|
|
|
|
func TestLegacyExternalAdvisoryRequiresPerRequestAcknowledgementBeforeUpstreamCalls(t *testing.T) {
|
|
t.Skip("acknowledgement is enforced by the cognitive gateway")
|
|
called := false
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
|
|
defer upstream.Close()
|
|
handler, err := newHandlerWithAdvisoryMode(upstream.URL, upstream.URL, upstream.URL, upstream.Client(), legacyExternalMode, upstream.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home","egress_acknowledged":false}`))
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusBadRequest || called {
|
|
t.Fatalf("status=%d upstream_called=%t", response.Code, called)
|
|
}
|
|
}
|
|
|
|
func TestLegacyExternalAdvisoryUsesOnlyServerSelectedRouteAndBinding(t *testing.T) {
|
|
t.Skip("superseded by provider-neutral gateway route test")
|
|
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
switch r.URL.Path {
|
|
case "/v1/creator/sessions/current":
|
|
_, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`)
|
|
case "/v1/creator/focus":
|
|
_, _ = io.WriteString(w, `{"ID":"focus-1","ActorID":"creator-actor","SessionID":"focus-session","Workspace":"home","ScopeID":"trusted-scope","Active":true}`)
|
|
default:
|
|
t.Fatalf("unexpected core path %s", r.URL.Path)
|
|
}
|
|
}))
|
|
defer core.Close()
|
|
cognitiveRuntime := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/v1/advisories" {
|
|
t.Fatalf("unexpected cognitive path %s", r.URL.Path)
|
|
}
|
|
var trusted cognitive.GatewayAdvisoryRequest
|
|
if err := json.NewDecoder(r.Body).Decode(&trusted); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(trusted.Context) != 0 || trusted.Message != "help" {
|
|
t.Fatalf("unexpected trusted request %+v", trusted)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: trusted.RequestID, AdvisoryText: "Проверьте границы.", FinishReason: cognitive.FinishCompleted, Usage: &cognitive.ProviderUsage{InputUnits: 2, OutputUnits: 3}})
|
|
}))
|
|
defer cognitiveRuntime.Close()
|
|
handler, err := newHandlerWithAdvisoryMode(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), legacyExternalMode, cognitiveRuntime.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home","egress_acknowledged":true}`))
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "Проверьте границы") {
|
|
t.Fatalf("response=%d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLegacyExternalAdvisoryRejectsBrowserSelectedBinding(t *testing.T) {
|
|
handler, err := newHandlerWithAdvisoryMode("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, legacyExternalMode)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home","egress_acknowledged":true,"provider_binding_id":"attacker"}`))
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusBadRequest {
|
|
t.Fatalf("status=%d body=%s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestLegacyExternalAdvisoryResponseValidationFailsClosed(t *testing.T) {
|
|
request := cognitive.GatewayAdvisoryRequest{Version: cognitive.CreatorAdvisoryV1, RequestID: "request-1", MaxOutputBytes: 16}
|
|
valid := cognitive.GatewayAdvisoryResponse{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "совет", FinishReason: cognitive.FinishCompleted}
|
|
if !validAdvisoryResponse(valid, request) {
|
|
t.Fatal("valid completed response was rejected")
|
|
}
|
|
invalid := []cognitive.GatewayAdvisoryResponse{
|
|
{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, FinishReason: cognitive.FinishCompleted},
|
|
{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "unexpected", FinishReason: cognitive.FinishTimeout},
|
|
{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "0123456789abcdefg", FinishReason: cognitive.FinishCompleted},
|
|
{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "ok", ReferencedExcerptIDs: []cognitive.ContextExcerptID{"untrusted"}, FinishReason: cognitive.FinishCompleted},
|
|
{Version: cognitive.CreatorAdvisoryV1, RequestID: request.RequestID, AdvisoryText: "ok", FinishReason: cognitive.FinishCompleted, Usage: &cognitive.ProviderUsage{InputUnits: -1}},
|
|
}
|
|
for index, response := range invalid {
|
|
if validAdvisoryResponse(response, request) {
|
|
t.Fatalf("invalid response %d was accepted: %+v", index, response)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAdvisoryRequiresSessionBeforeUpstreamCalls(t *testing.T) {
|
|
called := false
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
|
|
defer upstream.Close()
|
|
handler, err := newHandler(upstream.URL, upstream.URL, upstream.URL, upstream.Client(), upstream.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home"}`))
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusUnauthorized || called {
|
|
t.Fatalf("status=%d upstream_called=%t", response.Code, called)
|
|
}
|
|
}
|
|
|
|
func TestAdvisoryAssemblesTrustedIdentityAndScope(t *testing.T) {
|
|
longMessage := strings.Repeat("a", 16*1024)
|
|
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "Bearer bounded-session" {
|
|
t.Fatal("missing bounded session")
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
switch r.URL.Path {
|
|
case "/v1/creator/sessions/current":
|
|
_, _ = io.WriteString(w, `{"ActorID":"creator-actor","PrincipalID":"creator-principal","ExpiresAtUnix":2000000000}`)
|
|
case "/v1/creator/focus":
|
|
if r.URL.Query().Get("session_id") != "focus-session" || r.URL.Query().Get("workspace_id") != "home" {
|
|
t.Fatalf("unexpected focus query: %s", r.URL.RawQuery)
|
|
}
|
|
_, _ = io.WriteString(w, `{"ID":"focus-1","ActorID":"creator-actor","SessionID":"focus-session","Workspace":"home","ProjectID":"domovoy","GoalID":"goal-1","ScopeID":"trusted-scope","Active":true}`)
|
|
default:
|
|
t.Fatalf("unexpected core path %s", r.URL.Path)
|
|
}
|
|
}))
|
|
defer core.Close()
|
|
cognitive := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
body, _ := io.ReadAll(r.Body)
|
|
text := string(body)
|
|
var trusted cognitive.AdvisoryRequest
|
|
if err := json.Unmarshal(body, &trusted); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if trusted.ActorID != "creator-actor" || trusted.PrincipalID != "creator-principal" || trusted.ScopeID != "trusted-scope" || trusted.Message != longMessage || strings.Contains(text, "attacker") {
|
|
t.Fatal("cognitive request was not assembled from trusted context")
|
|
}
|
|
var request map[string]any
|
|
_ = json.Unmarshal(body, &request)
|
|
_, _ = fmt.Fprintf(w, `{"Version":"creator-advisory-v1","RequestID":%q,"AdvisoryText":"","ReferencedExcerptIDs":null,"FinishReason":"PROVIDER_UNAVAILABLE","Usage":null}`, request["RequestID"])
|
|
}))
|
|
defer cognitive.Close()
|
|
handler, err := newHandler(core.URL, "http://continuity.invalid", "http://commit.invalid", core.Client(), cognitive.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(fmt.Sprintf(`{"message":%q,"session_id":"focus-session","workspace_id":"home"}`, longMessage)))
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusOK || !strings.Contains(response.Body.String(), "PROVIDER_UNAVAILABLE") {
|
|
t.Fatalf("response=%d %s", response.Code, response.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAdvisoryRejectsBrowserAuthorityFieldsBeforeCognitiveCall(t *testing.T) {
|
|
called := false
|
|
cognitive := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true }))
|
|
defer cognitive.Close()
|
|
handler, err := newHandler("http://core.invalid", "http://continuity.invalid", "http://commit.invalid", http.DefaultClient, cognitive.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
request := httptest.NewRequest(http.MethodPost, "/api/advisory", strings.NewReader(`{"message":"help","session_id":"focus-session","workspace_id":"home","ActorID":"attacker"}`))
|
|
request.Header.Set("Authorization", "Bearer bounded-session")
|
|
response := httptest.NewRecorder()
|
|
handler.ServeHTTP(response, request)
|
|
if response.Code != http.StatusBadRequest || called {
|
|
t.Fatalf("status=%d cognitive_called=%t", response.Code, called)
|
|
}
|
|
}
|