Files
go-sip/internal/configread/current_test.go
T

195 lines
8.0 KiB
Go

package configread
import (
"context"
"encoding/json"
"errors"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"sync/atomic"
"testing"
)
func currentExample(t *testing.T, name string) []byte {
t.Helper()
raw, err := os.ReadFile(filepath.Join("..", "..", "contracts", "local", "examples", name+".json"))
if err != nil {
t.Fatal(err)
}
return raw
}
func TestReadCurrentTaskSnapshot(t *testing.T) {
for _, mode := range []string{"asr", "full"} {
t.Run(mode, func(t *testing.T) {
responses := map[string][]byte{
"/internal/v1/dispatcher/sip": currentExample(t, "config-read-sip"),
"/internal/v1/dispatcher/ai-providers": currentExample(t, "config-read-providers"),
"/internal/v1/dispatcher/task/task-" + mode: currentExample(t, "config-read-task-"+mode),
"/internal/v1/dispatcher/tenant/1001/quota": currentExample(t, "config-read-quota"),
}
calls := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
calls++
if r.Method != http.MethodGet || r.Header.Get("X-DISPATCHER-ID") != "c046b893-8628-4589-ae50-619d049248a6" || r.Header.Get("X-DISPATCHER-SECRET-KEY") != "test-secret" {
t.Errorf("unexpected method/headers: %s", r.URL.Path)
w.WriteHeader(http.StatusBadRequest)
return
}
body, ok := responses[r.URL.Path]
if !ok {
t.Errorf("unexpected path %s", r.URL.Path)
w.WriteHeader(http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(body)
}))
defer server.Close()
client, err := NewClient(server.URL, "c046b893-8628-4589-ae50-619d049248a6", "test-secret", server.Client())
if err != nil {
t.Fatal(err)
}
snapshot, err := client.ReadCurrentTask(context.Background(), "task-"+mode, 1001)
if err != nil {
t.Fatal(err)
}
if calls != 4 || snapshot.SIP.Revision != 8 || snapshot.Task.TenantID != 1001 || snapshot.Quota.TenantID != 1001 {
t.Fatalf("unexpected request count or identity: calls=%d snapshot=%+v", calls, snapshot)
}
if got := snapshot.Providers["asr-example"].Credential; got != "example-only-not-a-real-secret" {
t.Fatalf("credential not passed unchanged: %q", got)
}
if mode == "asr" && (snapshot.Task.Agent.Mode != "asr_only" || strings.Contains(string(snapshot.Task.Agent.Raw), `"llm"`)) {
t.Fatalf("ASR-only Agent picked up an LLM: %s", snapshot.Task.Agent.Raw)
}
if mode == "full" {
var agent struct {
LLM struct {
Temperature *float64 `json:"temperature"`
} `json:"llm"`
Conversation struct {
AllowInterrupt *bool `json:"allow_interrupt"`
} `json:"conversation"`
}
if err := json.Unmarshal(snapshot.Task.Agent.Raw, &agent); err != nil || agent.LLM.Temperature == nil || *agent.LLM.Temperature != 0 || agent.Conversation.AllowInterrupt == nil || *agent.Conversation.AllowInterrupt {
t.Fatalf("explicit zero/false lost: %v, %s", err, snapshot.Task.Agent.Raw)
}
}
})
}
}
func TestReadCurrentSIPWithoutTasks(t *testing.T) {
id := "c046b893-8628-4589-ae50-619d049248a6"
requests := 0
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests++
if r.URL.Path != "/internal/v1/dispatcher/sip" {
t.Errorf("startup SIP read requested unexpected path %q", r.URL.Path)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(currentExample(t, "config-read-sip"))
}))
defer server.Close()
client, err := NewClient(server.URL, id, "test-secret", server.Client())
if err != nil {
t.Fatal(err)
}
sip, err := client.ReadCurrentSIP(context.Background())
if err != nil || sip.Revision != 8 || requests != 1 {
t.Fatalf("startup SIP config: %+v, %v, requests=%d", sip, err, requests)
}
}
func TestReadCurrentTaskDoesNotReuseConfigAfterHTTPFailure(t *testing.T) {
const dispatcherID = "c046b893-8628-4589-ae50-619d049248a6"
responses := map[string][]byte{
"/internal/v1/dispatcher/sip": currentExample(t, "config-read-sip"),
"/internal/v1/dispatcher/ai-providers": currentExample(t, "config-read-providers"),
"/internal/v1/dispatcher/task/task-asr": currentExample(t, "config-read-task-asr"),
"/internal/v1/dispatcher/tenant/1001/quota": currentExample(t, "config-read-quota"),
}
for _, tc := range []struct{ path, owner string }{
{"/internal/v1/dispatcher/sip", "SIP"},
{"/internal/v1/dispatcher/ai-providers", "AI providers"},
{"/internal/v1/dispatcher/task/task-asr", "task configuration"},
{"/internal/v1/dispatcher/tenant/1001/quota", "tenant quota"},
} {
t.Run(tc.owner, func(t *testing.T) {
var unavailable atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
if unavailable.Load() && r.URL.Path == tc.path {
w.WriteHeader(http.StatusServiceUnavailable)
_, _ = w.Write([]byte(`{"resource":"error","error":{"code":"unavailable","message":"try later"}}`))
return
}
body, ok := responses[r.URL.Path]
if !ok {
http.NotFound(w, r)
return
}
_, _ = w.Write(body)
}))
defer server.Close()
client, err := NewClient(server.URL, dispatcherID, "isolated-secret", server.Client())
if err != nil {
t.Fatal(err)
}
if _, err := client.ReadCurrentTask(context.Background(), "task-asr", 1001); err != nil {
t.Fatalf("approved resources failed before the outage: %v", err)
}
unavailable.Store(true)
_, err = client.ReadCurrentTask(context.Background(), "task-asr", 1001)
var httpErr *HTTPError
if !errors.As(err, &httpErr) || httpErr.StatusCode != http.StatusServiceUnavailable {
t.Fatalf("unavailable %s reused cached configuration or lost status: %v", tc.owner, err)
}
if !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(tc.owner)) || strings.Contains(err.Error(), "isolated-secret") {
t.Fatalf("HTTP failure lost resource ownership or leaked credentials: %v", err)
}
})
}
}
func TestReadCurrentTaskRejectsMismatchedOwnerAndMalformedSIP(t *testing.T) {
for _, mode := range []string{"owner", "sip-revision", "provider-ref", "provider-disabled", "provider-wrong-role"} {
t.Run(mode, func(t *testing.T) {
responses := map[string][]byte{
"/internal/v1/dispatcher/sip": currentExample(t, "config-read-sip"),
"/internal/v1/dispatcher/ai-providers": currentExample(t, "config-read-providers"),
"/internal/v1/dispatcher/task/task-asr": currentExample(t, "config-read-task-asr"),
"/internal/v1/dispatcher/tenant/1001/quota": currentExample(t, "config-read-quota"),
}
switch mode {
case "owner":
responses["/internal/v1/dispatcher/task/task-asr"] = []byte(strings.Replace(string(responses["/internal/v1/dispatcher/task/task-asr"]), `"tenant_id":1001`, `"tenant_id":1002`, 1))
case "sip-revision":
responses["/internal/v1/dispatcher/sip"] = currentExample(t, "invalid/config-read-sip-missing-revision")
case "provider-ref":
responses["/internal/v1/dispatcher/ai-providers"] = currentExample(t, "invalid/config-read-provider-ref")
case "provider-disabled":
responses["/internal/v1/dispatcher/ai-providers"] = []byte(strings.Replace(string(responses["/internal/v1/dispatcher/ai-providers"]), `"enabled":true`, `"enabled":false`, 1))
case "provider-wrong-role":
responses["/internal/v1/dispatcher/ai-providers"] = []byte(strings.Replace(string(responses["/internal/v1/dispatcher/ai-providers"]), `"role":"asr"`, `"role":"tts"`, 1))
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write(responses[r.URL.Path])
}))
defer server.Close()
client, err := NewClient(server.URL, "c046b893-8628-4589-ae50-619d049248a6", "test-secret", server.Client())
if err != nil {
t.Fatal(err)
}
if _, err := client.ReadCurrentTask(context.Background(), "task-asr", 1001); err == nil || strings.Contains(err.Error(), "example-only-not-a-real-secret") {
t.Fatalf("expected redacted fail-closed error, got %v", err)
}
})
}
}