296 lines
15 KiB
Go
296 lines
15 KiB
Go
package growthreport
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestAgentRouterClientExecutesToolsThenReturnsStructuredCopy(t *testing.T) {
|
|
turn := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
turn++
|
|
if r.URL.Path != "/v1/chat/completions" {
|
|
t.Fatalf("path = %q", r.URL.Path)
|
|
}
|
|
if r.Header.Get("Authorization") != "Bearer key" || r.Header.Get("User-Agent") != agentRouterUserAgent {
|
|
t.Fatalf("provider headers are missing")
|
|
}
|
|
var request map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if request["reasoning_effort"] != "medium" || request["parallel_tool_calls"] != false {
|
|
t.Fatalf("request controls = %#v", request)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if turn == 1 {
|
|
_, _ = w.Write([]byte(`{"id":"chat-1","choices":[{"finish_reason":"tool_calls","message":{"role":"assistant","content":null,"tool_calls":[{"id":"call_1","type":"function","function":{"name":"get_event_evidence","arguments":"{\"evidenceId\":\"event_teacher\"}"}}]}}],"usage":{"prompt_tokens":2,"completion_tokens":1}}`))
|
|
return
|
|
}
|
|
messages := request["messages"].([]any)
|
|
toolMessage := messages[len(messages)-1].(map[string]any)
|
|
if toolMessage["role"] != "tool" || toolMessage["tool_call_id"] != "call_1" {
|
|
t.Fatalf("tool result message = %#v", toolMessage)
|
|
}
|
|
_, _ = w.Write([]byte(`{"id":"chat-2","choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"{\"eventRecommendations\":[{\"evidenceId\":\"event_teacher\",\"priorityPercent\":40,\"stance\":\"PROMOTE\",\"relevanceReason\":\"این مناسبت با الگوی فروش کسبوکار همخوان است.\",\"insightBasis\":\"BUSINESS_PROFILE\"},{\"evidenceId\":\"event_yalda\",\"priorityPercent\":35,\"stance\":\"PREPARE\",\"relevanceReason\":\"این رویداد با کانال فروش شما مرتبط است.\",\"insightBasis\":\"BUSINESS_PROFILE\"},{\"evidenceId\":\"event_nowruz\",\"priorityPercent\":25,\"stance\":\"CAUTION\",\"relevanceReason\":\"این بازه برای برنامهریزی عرضه مناسب است.\",\"insightBasis\":\"BUSINESS_PROFILE\"}]}"}}],"usage":{"prompt_tokens":3,"completion_tokens":4}}`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
executor := &testTools{}
|
|
client := NewAgentRouterClient(AgentRouterConfig{BaseURL: server.URL, APIKey: "key", Model: "gpt-test", Timeout: time.Second, EnableTools: true}, server.Client())
|
|
var generator CopyGenerator = client
|
|
copy, usage, err := generator.GenerateWithTools(context.Background(), json.RawMessage(`{"events":[]}`), profileKnownEvidence(), executor)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(copy.EventRecommendations) != 3 || executor.calls != 1 || executor.evidenceID != "event_teacher" || turn != 2 || usage.InputTokens != 5 || usage.OutputTokens != 5 || usage.ProviderRequests != 2 {
|
|
t.Fatalf("copy=%+v calls=%d evidenceID=%q turns=%d usage=%+v", copy, executor.calls, executor.evidenceID, turn, usage)
|
|
}
|
|
}
|
|
|
|
func TestAgentRouterClientRetriesRejectedSemanticOutput(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
requests++
|
|
copy := validRankedCopy()
|
|
if requests == 1 {
|
|
copy.EventRecommendations[0].RelevanceReason = "رشد ۲۰ درصدی پیشبینی میشود."
|
|
}
|
|
content, err := json.Marshal(copy)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": "chat", "choices": []any{map[string]any{
|
|
"finish_reason": "stop", "message": map[string]any{"role": "assistant", "content": string(content)},
|
|
}}, "usage": map[string]any{"prompt_tokens": 2, "completion_tokens": 1},
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewAgentRouterClient(AgentRouterConfig{BaseURL: server.URL, APIKey: "key", Model: "gpt-test", Timeout: time.Second}, server.Client())
|
|
_, usage, err := client.GenerateWithTools(context.Background(), json.RawMessage(`{"events":[]}`), profileKnownEvidence(), nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if requests != 2 || usage.ProviderRequests != 2 {
|
|
t.Fatalf("requests=%d usage=%+v", requests, usage)
|
|
}
|
|
}
|
|
|
|
func TestAgentRouterClientPreservesSemanticErrorWhenRetryFails(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
requests++
|
|
if requests == 1 {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_, _ = w.Write([]byte(`{"id":"invalid","choices":[{"finish_reason":"stop","message":{"role":"assistant","content":"{\"eventRecommendations\":[]}"}}]}`))
|
|
return
|
|
}
|
|
http.Error(w, "provider unavailable", http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewAgentRouterClient(AgentRouterConfig{BaseURL: server.URL, APIKey: "key", Model: "gpt-test", Timeout: time.Second}, server.Client())
|
|
_, _, err := client.GenerateWithTools(context.Background(), json.RawMessage(`{"events":[]}`), profileKnownEvidence(), nil)
|
|
if err == nil || !strings.Contains(err.Error(), "invalid event recommendation count") || !strings.Contains(err.Error(), "http 500") {
|
|
t.Fatalf("error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestAgentRouterClientForcesFinalResponseAtToolLimit(t *testing.T) {
|
|
requests := 0
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
requests++
|
|
var request map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
if requests <= maxAvalAIToolCalls {
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": "chat", "choices": []any{map[string]any{
|
|
"finish_reason": "tool_calls", "message": map[string]any{
|
|
"role": "assistant", "tool_calls": []any{map[string]any{
|
|
"id": "call", "type": "function", "function": map[string]any{"name": "get_growth_context", "arguments": "{}"},
|
|
}},
|
|
},
|
|
}}, "usage": map[string]any{"prompt_tokens": 1, "completion_tokens": 1},
|
|
})
|
|
return
|
|
}
|
|
if _, hasTools := request["tools"]; hasTools {
|
|
t.Error("tools remain available on the forced final turn")
|
|
}
|
|
content, err := json.Marshal(validRankedCopy())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": "final", "choices": []any{map[string]any{
|
|
"finish_reason": "stop", "message": map[string]any{"role": "assistant", "content": string(content)},
|
|
}}, "usage": map[string]any{"prompt_tokens": 1, "completion_tokens": 1},
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
executor := &testTools{}
|
|
client := NewAgentRouterClient(AgentRouterConfig{BaseURL: server.URL, APIKey: "key", Model: "gpt-test", Timeout: time.Second, EnableTools: true}, server.Client())
|
|
_, usage, err := client.GenerateWithTools(context.Background(), json.RawMessage(`{"events":[]}`), profileKnownEvidence(), executor)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if requests != maxAvalAIProviderTurns || executor.calls != maxAvalAIToolCalls || usage.ProviderRequests != int64(maxAvalAIProviderTurns) {
|
|
t.Fatalf("requests=%d calls=%d usage=%+v", requests, executor.calls, usage)
|
|
}
|
|
}
|
|
|
|
func TestAgentRouterClientDefaultsToDirectStructuredGeneration(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
var request map[string]any
|
|
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, hasTools := request["tools"]; hasTools {
|
|
t.Error("default AgentRouter request unexpectedly advertises tools")
|
|
}
|
|
content, err := json.Marshal(validRankedCopy())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
|
"id": "direct", "choices": []any{map[string]any{
|
|
"finish_reason": "stop", "message": map[string]any{"role": "assistant", "content": string(content)},
|
|
}}, "usage": map[string]any{"prompt_tokens": 1, "completion_tokens": 1},
|
|
})
|
|
}))
|
|
defer server.Close()
|
|
|
|
client := NewAgentRouterClient(AgentRouterConfig{BaseURL: server.URL, APIKey: "key", Model: "gpt-test", Timeout: time.Second}, server.Client())
|
|
_, usage, err := client.GenerateWithTools(context.Background(), json.RawMessage(`{"events":[]}`), profileKnownEvidence(), &testTools{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if usage.ProviderRequests != 1 {
|
|
t.Fatalf("usage=%+v", usage)
|
|
}
|
|
}
|
|
|
|
func TestParseAgentRouterCopyAdaptsBareRecommendationArray(t *testing.T) {
|
|
known := KnownEvidence{
|
|
"event:education-enrollment-autumn-1405": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
"event:cyber-monday-2026": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
"event:black-friday-2026": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
}
|
|
message := agentRouterChatMessage{Content: `[
|
|
{"eventId":"education-enrollment-autumn-1405","priorityShare":50,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","reason":"همخوانی مستقیم با آموزش آنلاین؛ مرجع شواهد: event:education-enrollment-autumn-1405"},
|
|
{"eventId":"cyber-monday-2026","priorityShare":30,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","reason":"تناسب موضوعی با خدمات شبکه و اینترنت؛ مرجع شواهد: event:cyber-monday-2026"},
|
|
{"eventId":"black-friday-2026","priorityShare":20,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","reason":"فرصتی تجاری برای تقویت فروش آنلاین؛ مرجع شواهد: event:black-friday-2026"}
|
|
]`}
|
|
|
|
copy, err := parseAgentRouterCopy(message, known)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(copy.EventRecommendations) != 3 || copy.EventRecommendations[0].EvidenceID != "event:education-enrollment-autumn-1405" || copy.EventRecommendations[0].PriorityPercent != 50 {
|
|
t.Fatalf("copy=%+v", copy)
|
|
}
|
|
if copy.EventRecommendations[0].RelevanceReason != "همخوانی مستقیم با آموزش آنلاین" {
|
|
t.Fatalf("reason=%q", copy.EventRecommendations[0].RelevanceReason)
|
|
}
|
|
}
|
|
|
|
func TestParseAgentRouterCopyEnforcesDeterministicStancePolicy(t *testing.T) {
|
|
known := KnownEvidence{
|
|
"event:decline": {InsightBasis: InsightBasisBusinessProfile, MaterialDecline: true, Promotable: true},
|
|
"event:prepare": {InsightBasis: InsightBasisBusinessProfile, Promotable: false},
|
|
"event:promote": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
}
|
|
message := agentRouterChatMessage{Content: `[
|
|
{"eventId":"decline","priorityShare":50,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","reason":"افت معنادار پیشین نیازمند رویکرد محتاطانه است"},
|
|
{"eventId":"prepare","priorityShare":30,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","reason":"این فرصت برای آماده سازی عملیات کسب و کار مناسب است"},
|
|
{"eventId":"promote","priorityShare":20,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","reason":"این فرصت با فروش آنلاین کسب و کار هم خوانی دارد"}
|
|
]`}
|
|
|
|
copy, err := parseAgentRouterCopy(message, known)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if copy.EventRecommendations[0].Stance != StanceCaution {
|
|
t.Fatalf("decline stance=%q", copy.EventRecommendations[0].Stance)
|
|
}
|
|
if copy.EventRecommendations[1].Stance != StancePrepare {
|
|
t.Fatalf("non-promotable stance=%q", copy.EventRecommendations[1].Stance)
|
|
}
|
|
if copy.EventRecommendations[2].Stance != StancePromote {
|
|
t.Fatalf("promotable stance=%q", copy.EventRecommendations[2].Stance)
|
|
}
|
|
}
|
|
|
|
func TestParseAgentRouterCopyRemovesRedundantEvidenceSuffixFromObject(t *testing.T) {
|
|
known := KnownEvidence{
|
|
"event:education-enrollment-autumn-1405": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
"event:cyber-monday-2026": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
"event:black-friday-2026": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
}
|
|
message := agentRouterChatMessage{Content: `{"eventRecommendations":[
|
|
{"evidenceId":"event:education-enrollment-autumn-1405","priorityPercent":45,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","relevanceReason":"با آموزش آنلاین هم خوانی مستقیم دارد؛ شواهد event:education-enrollment-autumn-1405 ارتباط مستقیم را نشان می دهد."},
|
|
{"evidenceId":"event:cyber-monday-2026","priorityPercent":35,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","relevanceReason":"با خدمات شبکه و اینترنت تناسب دارد؛ مستند به event:cyber-monday-2026."},
|
|
{"evidenceId":"event:black-friday-2026","priorityPercent":20,"stance":"CAUTION","insightBasis":"BUSINESS_PROFILE","relevanceReason":"برای فروش آنلاین قابل استفاده است؛ مستند به event:black-friday-2026."}
|
|
]}`}
|
|
|
|
copy, err := parseAgentRouterCopy(message, known)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if copy.EventRecommendations[0].RelevanceReason != "با آموزش آنلاین هم خوانی مستقیم دارد" {
|
|
t.Fatalf("reason=%q", copy.EventRecommendations[0].RelevanceReason)
|
|
}
|
|
}
|
|
|
|
func TestParseAgentRouterCopySelectsTopThreeFromOverCompleteObject(t *testing.T) {
|
|
known := KnownEvidence{
|
|
"event:education": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
"event:black": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
"event:cyber": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
"event:yalda": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
"event:nowruz": {InsightBasis: InsightBasisBusinessProfile, Promotable: true},
|
|
}
|
|
message := agentRouterChatMessage{Content: `{"eventRecommendations":[
|
|
{"evidenceId":"event:yalda","priorityPercent":15,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","relevanceReason":"با خرید مناسبتی و فروش حضوری هم خوانی دارد."},
|
|
{"evidenceId":"event:black","priorityPercent":25,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","relevanceReason":"برای تقویت فروش آنلاین و تبلیغات مناسب است."},
|
|
{"evidenceId":"event:education","priorityPercent":30,"stance":"PROMOTE","insightBasis":"BUSINESS_PROFILE","relevanceReason":"با کانال های فعال کسب و کار هم راستا است."},
|
|
{"evidenceId":"event:nowruz","priorityPercent":10,"stance":"PREPARE","insightBasis":"BUSINESS_PROFILE","relevanceReason":"برای آماده سازی پیشنهادهای فروش مناسب است."},
|
|
{"evidenceId":"event:cyber","priorityPercent":20,"stance":"CAUTION","insightBasis":"BUSINESS_PROFILE","relevanceReason":"با فروش از مسیر وب سایت تناسب دارد."}
|
|
]}`}
|
|
|
|
copy, err := parseAgentRouterCopy(message, known)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(copy.EventRecommendations) != 3 {
|
|
t.Fatalf("recommendations=%d", len(copy.EventRecommendations))
|
|
}
|
|
wantIDs := []string{"event:education", "event:black", "event:cyber"}
|
|
total := 0
|
|
for index, recommendation := range copy.EventRecommendations {
|
|
if recommendation.EvidenceID != wantIDs[index] {
|
|
t.Fatalf("recommendation[%d].evidenceId=%q", index, recommendation.EvidenceID)
|
|
}
|
|
if index > 0 && recommendation.PriorityPercent > copy.EventRecommendations[index-1].PriorityPercent {
|
|
t.Fatalf("priorities are not descending: %+v", copy.EventRecommendations)
|
|
}
|
|
total += recommendation.PriorityPercent
|
|
}
|
|
if total != 100 {
|
|
t.Fatalf("priority total=%d", total)
|
|
}
|
|
}
|