feat: publish sanitized custom.97 source
This commit is contained in:
1022 files changed
+69903
-19207
No files matched your search
+441
-19
@@ -3,6 +3,7 @@ package controller
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"github.com/QuantumNous/new-api/common"
|
||||
"github.com/QuantumNous/new-api/model"
|
||||
@@ -23,6 +24,8 @@ import (
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
@@ -289,7 +292,7 @@ func TestDetectionModelTracePaidRoundAndResume(t *testing.T) {
|
||||
assert.Equal(t, 982, owner.Quota)
|
||||
}
|
||||
|
||||
func TestCandyAutomaticRoundAndResume(t *testing.T) {
|
||||
func TestVisionAutomaticRoundAndResume(t *testing.T) {
|
||||
for _, dialect := range []string{"sqlite", "mysql", "postgres"} {
|
||||
t.Run(dialect, func(t *testing.T) {
|
||||
oldRedis := common.RedisEnabled
|
||||
@@ -308,7 +311,7 @@ func TestCandyAutomaticRoundAndResume(t *testing.T) {
|
||||
}
|
||||
driver = postgres.Open(os.Getenv("TEST_POSTGRES_DSN"))
|
||||
}
|
||||
db, err := gorm.Open(driver, &gorm.Config{NamingStrategy: schema.NamingStrategy{TablePrefix: fmt.Sprintf("candy_%d_", time.Now().UnixNano())}})
|
||||
db, err := gorm.Open(driver, &gorm.Config{NamingStrategy: schema.NamingStrategy{TablePrefix: fmt.Sprintf("vision_%d_", time.Now().UnixNano())}})
|
||||
require.NoError(t, err)
|
||||
conn, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
@@ -329,35 +332,39 @@ func TestCandyAutomaticRoundAndResume(t *testing.T) {
|
||||
calls := 0
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Less(t, calls, 1)
|
||||
answer := "21"
|
||||
answer := "这是音箱"
|
||||
raw, readErr := io.ReadAll(r.Body)
|
||||
require.NoError(t, readErr)
|
||||
assert.Contains(t, gjson.GetBytes(raw, "input.0.content").String(), "圆形 7 9 8")
|
||||
assert.NotContains(t, gjson.GetBytes(raw, "input.0.content").String(), "21")
|
||||
assert.Equal(t, "medium", gjson.GetBytes(raw, "reasoning.effort").String())
|
||||
assert.Equal(t, "不搜索,告诉我这是什么", gjson.GetBytes(raw, "input.0.content.0.text").String())
|
||||
assert.Equal(t, service.VisionImageURL, gjson.GetBytes(raw, "input.0.content.1.image_url").String())
|
||||
assert.Equal(t, "none", gjson.GetBytes(raw, "tool_choice").String())
|
||||
assert.Empty(t, gjson.GetBytes(raw, "tools").Array())
|
||||
calls++
|
||||
body, encodeErr := common.Marshal(map[string]any{"status": "completed", "usage": map[string]int{"input_tokens": 10, "output_tokens": 2}, "output": []any{map[string]any{"type": "message", "content": []any{map[string]string{"type": "output_text", "text": answer}}}}})
|
||||
require.NoError(t, encodeErr)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(body)
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
_, _ = fmt.Fprint(w, "data: {\"type\":\"response.output_text.delta\",\"delta\":\"这是音箱\"}\n\n")
|
||||
_, _ = fmt.Fprintf(w, "data: {\"type\":\"response.completed\",\"response\":%s}\n\n", body)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
url := upstream.URL
|
||||
channel := model.Channel{Type: 1, Key: "test-only", BaseURL: &url, Status: common.ChannelStatusEnabled, Models: fingerprint.Model, Group: "candy-group"}
|
||||
channel := model.Channel{Type: 1, Key: "test-only", BaseURL: &url, Status: common.ChannelStatusEnabled, Models: fingerprint.Model, Group: "vision-group"}
|
||||
require.NoError(t, db.Create(&channel).Error)
|
||||
owner := model.User{Username: "meow-test", Quota: 1000, Status: common.UserStatusEnabled}
|
||||
require.NoError(t, db.Create(&owner).Error)
|
||||
price, err := common.Marshal(service.DetectionPrice{Snapshot: billingexpr.BillingSnapshot{ExprString: "p+c", ExprHash: billingexpr.ExprHashString("p+c"), GroupRatio: 1, QuotaPerUnit: 500000, ExprVersion: 1}, Request: billingexpr.RequestInput{Body: []byte(`{"max_output_tokens":4096,"reasoning":{"effort":"low"}}`)}})
|
||||
price, err := common.Marshal(service.DetectionPrice{Snapshot: billingexpr.BillingSnapshot{ExprString: "p+c", ExprHash: billingexpr.ExprHashString("p+c"), GroupRatio: 1, QuotaPerUnit: 500000, ExprVersion: 1}, Request: billingexpr.RequestInput{Body: []byte(`{"max_output_tokens":4096,"reasoning":{"effort":"medium"}}`)}})
|
||||
require.NoError(t, err)
|
||||
run := model.DetectionRun{ID: "meow-test", RequestKey: "meow-test", Kind: "candy", CreatedAt: time.Now().Unix(), ChannelID: channel.Id, Status: "pending", Deadline: time.Now().Unix() + 60, Reserved: 320, Price: string(price)}
|
||||
run := model.DetectionRun{ID: "meow-test", RequestKey: "meow-test", Kind: "vision", CreatedAt: time.Now().Unix(), ChannelID: channel.Id, Status: "pending", Deadline: time.Now().Unix() + 60, Reserved: 320, Price: string(price)}
|
||||
require.NoError(t, model.CreateDetection(&run))
|
||||
require.NoError(t, model.EnsureDetectionSchedule(context.Background(), model.DetectionSchedule{ChannelID: channel.Id, Kind: "candy", IntervalMinutes: 30}))
|
||||
require.NoError(t, model.EnsureDetectionSchedule(context.Background(), model.DetectionSchedule{ChannelID: channel.Id, Kind: "vision", IntervalMinutes: 30}))
|
||||
processDetection(context.Background(), run)
|
||||
require.NoError(t, db.First(&run, "id = ?", run.ID).Error)
|
||||
assert.Equal(t, "succeeded", run.Status)
|
||||
assert.Equal(t, "normal", run.Quality)
|
||||
assert.True(t, run.Settled)
|
||||
var clock model.DetectionSchedule
|
||||
require.NoError(t, db.First(&clock, "channel_id = ? AND kind = ?", channel.Id, "candy").Error)
|
||||
require.NoError(t, db.First(&clock, "channel_id = ? AND kind = ?", channel.Id, "vision").Error)
|
||||
assert.Equal(t, "normal", clock.LastValid)
|
||||
assert.Greater(t, clock.NextDueAt, run.FinishedAt)
|
||||
assert.Equal(t, 1, calls)
|
||||
@@ -370,19 +377,434 @@ func TestCandyAutomaticRoundAndResume(t *testing.T) {
|
||||
assert.Equal(t, 1000, owner.Quota)
|
||||
summaries, err := service.GroupDetectionSummaries(context.Background(), time.Now(), map[string]*service.AstraSamplingSummary{}, map[string]model.StatusPageGroup{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, summaries["candy-group"].Candy)
|
||||
assert.Equal(t, 1, summaries["candy-group"].Candy.Normal)
|
||||
assert.Equal(t, "21", summaries["candy-group"].Candy.History[0].Closest)
|
||||
assert.Zero(t, summaries["candy-group"].Benchmark.Normal)
|
||||
require.NotNil(t, summaries["vision-group"].Vision)
|
||||
assert.Equal(t, 1, summaries["vision-group"].Vision.Normal)
|
||||
assert.Equal(t, "这是音箱", summaries["vision-group"].Vision.History[0].Closest)
|
||||
require.NotNil(t, summaries["vision-group"].Vision.History[0].FirstTokenMS)
|
||||
assert.GreaterOrEqual(t, *summaries["vision-group"].Vision.History[0].FirstTokenMS, float64(0))
|
||||
assert.Zero(t, summaries["vision-group"].Benchmark.Normal)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCandyAnswers(t *testing.T) {
|
||||
func TestVisionAnswers(t *testing.T) {
|
||||
for _, tc := range []struct{ answer, quality string }{
|
||||
{"21", "normal"}, {" 21\n", "normal"}, {"28", "degraded"}, {"121", "degraded"}, {"21或28", "degraded"}, {"答案不是21", "degraded"}, {"", "degraded"},
|
||||
{"这是音响", "normal"}, {"蓝牙音箱", "normal"}, {" 音箱\n", "normal"},
|
||||
{"石头", "degraded"}, {"扬声器", "degraded"}, {"", "degraded"}, {"音 箱", "degraded"},
|
||||
} {
|
||||
t.Run(tc.answer, func(t *testing.T) { assert.Equal(t, tc.quality, service.CandyQuality(tc.answer)) })
|
||||
t.Run(tc.answer, func(t *testing.T) { assert.Equal(t, tc.quality, service.VisionQuality(tc.answer)) })
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisionFailuresAndRetiredDetections(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, answer string
|
||||
httpStatus int
|
||||
kind string
|
||||
malformed bool
|
||||
}{
|
||||
{name: "missing keyword", answer: "石头", httpStatus: 200, kind: "vision"},
|
||||
{name: "empty answer", httpStatus: 200, kind: "vision"},
|
||||
{name: "transport failure", httpStatus: 503, kind: "vision"},
|
||||
{name: "invalid response", httpStatus: 200, kind: "vision", malformed: true},
|
||||
{name: "retired candy", kind: "candy"},
|
||||
{name: "retired pelican", kind: "pelican"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
oldRedis := common.RedisEnabled
|
||||
common.RedisEnabled = false
|
||||
t.Cleanup(func() { common.RedisEnabled = oldRedis })
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
conn, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
conn.SetMaxOpenConns(1)
|
||||
old := model.DB
|
||||
model.DB = db
|
||||
t.Cleanup(func() { model.DB = old; _ = conn.Close() })
|
||||
require.NoError(t, db.AutoMigrate(&model.Channel{}, &model.DetectionRun{}, &model.DetectionCall{}, &model.DetectionSchedule{}, &model.User{}))
|
||||
calls := 0
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(tc.httpStatus)
|
||||
if tc.malformed {
|
||||
_, _ = io.WriteString(w, "broken")
|
||||
return
|
||||
}
|
||||
body, err := common.Marshal(map[string]any{"status": "completed", "output": []any{map[string]any{"type": "message", "content": []any{map[string]string{"type": "output_text", "text": tc.answer}}}}})
|
||||
require.NoError(t, err)
|
||||
_, _ = w.Write(body)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
url := upstream.URL
|
||||
channel := model.Channel{Type: 1, Key: "test", BaseURL: &url, Status: common.ChannelStatusEnabled, Models: fingerprint.Model}
|
||||
require.NoError(t, db.Create(&channel).Error)
|
||||
run := model.DetectionRun{ID: "failure", RequestKey: "failure", Kind: tc.kind, ChannelID: channel.Id, Status: "pending", Deadline: time.Now().Unix() + 60}
|
||||
require.NoError(t, model.CreateDetection(&run))
|
||||
require.NoError(t, model.EnsureDetectionSchedule(context.Background(), model.DetectionSchedule{ChannelID: channel.Id, Kind: "vision", IntervalMinutes: 30}))
|
||||
events, unsubscribe, err := subscribeGroupStatus()
|
||||
require.NoError(t, err)
|
||||
defer unsubscribe()
|
||||
processDetection(context.Background(), run)
|
||||
require.NoError(t, db.First(&run, "id = ?", run.ID).Error)
|
||||
expectedStatus, expectedQuality, expectedOutcome := "failed", "unknown", ""
|
||||
if tc.name == "missing keyword" {
|
||||
expectedStatus, expectedQuality, expectedOutcome = "succeeded", "degraded", "degraded"
|
||||
}
|
||||
assert.Equal(t, expectedStatus, run.Status)
|
||||
assert.True(t, run.Settled)
|
||||
if tc.kind == "vision" {
|
||||
assert.Equal(t, expectedQuality, run.Quality)
|
||||
select {
|
||||
case <-events:
|
||||
default:
|
||||
t.Fatal("settled vision did not publish")
|
||||
}
|
||||
assert.Equal(t, 1, calls)
|
||||
var clock model.DetectionSchedule
|
||||
require.NoError(t, db.First(&clock, "channel_id = ? AND kind = ?", channel.Id, "vision").Error)
|
||||
assert.Equal(t, expectedOutcome, clock.LastValid)
|
||||
} else {
|
||||
assert.Zero(t, calls)
|
||||
assert.Equal(t, "detection_retired", run.Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
func TestVisionPaidRoundAndResume(t *testing.T) {
|
||||
for _, dialect := range []string{"sqlite", "mysql", "postgres"} {
|
||||
t.Run(dialect, func(t *testing.T) {
|
||||
oldRedis := common.RedisEnabled
|
||||
common.RedisEnabled = false
|
||||
t.Cleanup(func() { common.RedisEnabled = oldRedis })
|
||||
var driver gorm.Dialector = sqlite.Open(":memory:")
|
||||
if dialect == "mysql" {
|
||||
if os.Getenv("TEST_MYSQL_DSN") == "" {
|
||||
t.Skip("TEST_MYSQL_DSN unset")
|
||||
}
|
||||
driver = mysql.Open(os.Getenv("TEST_MYSQL_DSN"))
|
||||
}
|
||||
if dialect == "postgres" {
|
||||
if os.Getenv("TEST_POSTGRES_DSN") == "" {
|
||||
t.Skip("TEST_POSTGRES_DSN unset")
|
||||
}
|
||||
driver = postgres.Open(os.Getenv("TEST_POSTGRES_DSN"))
|
||||
}
|
||||
db, err := gorm.Open(driver, &gorm.Config{NamingStrategy: schema.NamingStrategy{TablePrefix: fmt.Sprintf("vision_%d_", time.Now().UnixNano())}})
|
||||
require.NoError(t, err)
|
||||
conn, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
conn.SetMaxOpenConns(1)
|
||||
oldType, oldLogType := common.MainDatabaseType(), common.LogDatabaseType()
|
||||
common.SetDatabaseTypes(common.DatabaseType(dialect), common.DatabaseType(dialect))
|
||||
t.Cleanup(func() { common.SetDatabaseTypes(oldType, oldLogType) })
|
||||
old := model.DB
|
||||
model.DB = db
|
||||
t.Cleanup(func() {
|
||||
for _, row := range []any{&model.Channel{}, &model.DetectionRun{}, &model.DetectionCall{}, &model.User{}, &model.DetectionSchedule{}, &model.SystemTask{}} {
|
||||
require.NoError(t, db.Migrator().DropTable(row))
|
||||
}
|
||||
model.DB = old
|
||||
_ = conn.Close()
|
||||
})
|
||||
require.NoError(t, db.AutoMigrate(&model.Channel{}, &model.DetectionRun{}, &model.DetectionCall{}, &model.User{}, &model.DetectionSchedule{}, &model.SystemTask{}))
|
||||
calls := 0
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
require.Less(t, calls, 1)
|
||||
answer := "这是音箱"
|
||||
raw, readErr := io.ReadAll(r.Body)
|
||||
require.NoError(t, readErr)
|
||||
assert.Equal(t, "medium", gjson.GetBytes(raw, "reasoning.effort").String())
|
||||
assert.Equal(t, "不搜索,告诉我这是什么", gjson.GetBytes(raw, "input.0.content.0.text").String())
|
||||
assert.Equal(t, service.VisionImageURL, gjson.GetBytes(raw, "input.0.content.1.image_url").String())
|
||||
assert.Equal(t, "none", gjson.GetBytes(raw, "tool_choice").String())
|
||||
assert.Empty(t, gjson.GetBytes(raw, "tools").Array())
|
||||
calls++
|
||||
body, encodeErr := common.Marshal(map[string]any{"status": "completed", "usage": map[string]int{"input_tokens": 10, "output_tokens": 2}, "output": []any{map[string]any{"type": "message", "content": []any{map[string]string{"type": "output_text", "text": answer}}}}})
|
||||
require.NoError(t, encodeErr)
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
_, _ = fmt.Fprint(w, "data: {\"type\":\"response.output_text.delta\",\"delta\":\"这是音箱\"}\n\n")
|
||||
_, _ = fmt.Fprintf(w, "data: {\"type\":\"response.completed\",\"response\":%s}\n\n", body)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
url := upstream.URL
|
||||
channel := model.Channel{Type: 1, Key: "test-only", BaseURL: &url, Status: common.ChannelStatusEnabled, Models: fingerprint.Model, Group: "vision-group"}
|
||||
require.NoError(t, db.Create(&channel).Error)
|
||||
owner := model.User{Username: "meow-test", Quota: 1000, Status: common.UserStatusEnabled}
|
||||
require.NoError(t, db.Create(&owner).Error)
|
||||
price, err := common.Marshal(service.DetectionPrice{Snapshot: billingexpr.BillingSnapshot{ExprString: "p+c", ExprHash: billingexpr.ExprHashString("p+c"), GroupRatio: 1, QuotaPerUnit: 500000, ExprVersion: 1}, Request: billingexpr.RequestInput{Body: []byte(`{"max_output_tokens":4096,"reasoning":{"effort":"medium"}}`)}})
|
||||
require.NoError(t, err)
|
||||
run := model.DetectionRun{ID: "meow-test", RequestKey: "meow-test", UserID: owner.Id, Kind: "vision", CreatedAt: time.Now().Unix(), ChannelID: channel.Id, Status: "pending", Deadline: time.Now().Unix() + 60, Reserved: 320, Price: string(price)}
|
||||
require.NoError(t, model.CreateDetection(&run))
|
||||
require.NoError(t, model.EnsureDetectionSchedule(context.Background(), model.DetectionSchedule{ChannelID: channel.Id, Kind: "vision", IntervalMinutes: 30}))
|
||||
processDetection(context.Background(), run)
|
||||
require.NoError(t, db.First(&run, "id = ?", run.ID).Error)
|
||||
assert.Equal(t, "succeeded", run.Status)
|
||||
assert.Equal(t, "normal", run.Quality)
|
||||
assert.True(t, run.Settled)
|
||||
var clock model.DetectionSchedule
|
||||
require.NoError(t, db.First(&clock, "channel_id = ? AND kind = ?", channel.Id, "vision").Error)
|
||||
assert.Empty(t, clock.LastValid)
|
||||
assert.Zero(t, clock.NextDueAt, "manual detection does not change automatic schedule")
|
||||
assert.Equal(t, 1, calls)
|
||||
expectedCharge, err := service.DetectionUsageQuota(string(price), 10, 2, 0)
|
||||
require.NoError(t, err)
|
||||
assert.Positive(t, expectedCharge)
|
||||
assert.Equal(t, expectedCharge, run.Charged)
|
||||
require.NoError(t, db.First(&owner, owner.Id).Error)
|
||||
assert.Equal(t, 1000-expectedCharge, owner.Quota)
|
||||
processDetection(context.Background(), run)
|
||||
assert.Equal(t, 1, calls, "resume must not resend or recharge")
|
||||
require.NoError(t, db.First(&owner, owner.Id).Error)
|
||||
assert.Equal(t, 1000-expectedCharge, owner.Quota)
|
||||
summaries, err := service.GroupDetectionSummaries(context.Background(), time.Now(), map[string]*service.AstraSamplingSummary{}, map[string]model.StatusPageGroup{})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, summaries["vision-group"].Vision)
|
||||
assert.Equal(t, 1, summaries["vision-group"].Vision.Normal)
|
||||
assert.Equal(t, "这是音箱", summaries["vision-group"].Vision.History[0].Closest)
|
||||
require.NotNil(t, summaries["vision-group"].Vision.History[0].FirstTokenMS)
|
||||
assert.GreaterOrEqual(t, *summaries["vision-group"].Vision.History[0].FirstTokenMS, float64(0))
|
||||
assert.Zero(t, summaries["vision-group"].Benchmark.Normal)
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCialloResults(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name, answer string
|
||||
httpStatus int
|
||||
kind string
|
||||
malformed bool
|
||||
}{
|
||||
{name: "missing keyword", answer: "石头", httpStatus: 200, kind: "ciallo"},
|
||||
{name: "empty answer", httpStatus: 200, kind: "ciallo"},
|
||||
{name: "transport failure", httpStatus: 503, kind: "ciallo"},
|
||||
{name: "invalid response", httpStatus: 200, kind: "ciallo", malformed: true},
|
||||
{name: "passed", answer: "Ciallo~(∠・ω< )⌒☆ Hi there!", httpStatus: 200, kind: "ciallo"},
|
||||
{name: "plain punctuation", answer: "Ciallo~", httpStatus: 200, kind: "ciallo"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
oldRedis := common.RedisEnabled
|
||||
common.RedisEnabled = false
|
||||
t.Cleanup(func() { common.RedisEnabled = oldRedis })
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||
require.NoError(t, err)
|
||||
conn, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
conn.SetMaxOpenConns(1)
|
||||
old := model.DB
|
||||
model.DB = db
|
||||
t.Cleanup(func() { model.DB = old; _ = conn.Close() })
|
||||
require.NoError(t, db.AutoMigrate(&model.Channel{}, &model.DetectionRun{}, &model.DetectionCall{}, &model.DetectionSchedule{}, &model.User{}))
|
||||
calls := 0
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
calls++
|
||||
raw, _ := io.ReadAll(r.Body)
|
||||
assert.Equal(t, "ciallo", gjson.GetBytes(raw, "input.0.content").String())
|
||||
assert.Equal(t, "medium", gjson.GetBytes(raw, "reasoning.effort").String())
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(tc.httpStatus)
|
||||
if tc.malformed {
|
||||
_, _ = io.WriteString(w, "broken")
|
||||
return
|
||||
}
|
||||
body, err := common.Marshal(map[string]any{"status": "completed", "output": []any{map[string]any{"type": "message", "content": []any{map[string]string{"type": "output_text", "text": tc.answer}}}}})
|
||||
require.NoError(t, err)
|
||||
_, _ = w.Write(body)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
url := upstream.URL
|
||||
channel := model.Channel{Type: 1, Key: "test", BaseURL: &url, Status: common.ChannelStatusEnabled, Models: fingerprint.Model}
|
||||
require.NoError(t, db.Create(&channel).Error)
|
||||
run := model.DetectionRun{ID: "failure", RequestKey: "failure", Kind: tc.kind, ChannelID: channel.Id, Status: "pending", Deadline: time.Now().Unix() + 60}
|
||||
require.NoError(t, model.CreateDetection(&run))
|
||||
require.NoError(t, model.EnsureDetectionSchedule(context.Background(), model.DetectionSchedule{ChannelID: channel.Id, Kind: "ciallo", IntervalMinutes: 30}))
|
||||
events, unsubscribe, err := subscribeGroupStatus()
|
||||
require.NoError(t, err)
|
||||
defer unsubscribe()
|
||||
processDetection(context.Background(), run)
|
||||
require.NoError(t, db.First(&run, "id = ?", run.ID).Error)
|
||||
expectedStatus, expectedQuality, expectedOutcome := "failed", "unknown", ""
|
||||
if tc.name == "missing keyword" {
|
||||
expectedStatus, expectedQuality, expectedOutcome = "succeeded", "degraded", "degraded"
|
||||
}
|
||||
if tc.name == "passed" || tc.name == "plain punctuation" {
|
||||
expectedStatus, expectedQuality, expectedOutcome = "succeeded", "normal", "normal"
|
||||
}
|
||||
assert.Equal(t, expectedStatus, run.Status)
|
||||
assert.True(t, run.Settled)
|
||||
if tc.kind == "ciallo" {
|
||||
assert.Equal(t, expectedQuality, run.Quality)
|
||||
select {
|
||||
case <-events:
|
||||
default:
|
||||
t.Fatal("settled vision did not publish")
|
||||
}
|
||||
assert.Equal(t, 1, calls)
|
||||
var clock model.DetectionSchedule
|
||||
require.NoError(t, db.First(&clock, "channel_id = ? AND kind = ?", channel.Id, "ciallo").Error)
|
||||
assert.Equal(t, expectedOutcome, clock.LastValid)
|
||||
assert.Equal(t, 45, clock.IntervalMinutes)
|
||||
assert.Equal(t, int64(45*60), clock.NextDueAt-clock.LastFinishedAt)
|
||||
} else {
|
||||
assert.Zero(t, calls)
|
||||
assert.Equal(t, "detection_retired", run.Error)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCialloBotAuthorization(t *testing.T) {
|
||||
key := strings.Repeat("k", 32)
|
||||
t.Setenv("BOT_BALANCE_KEY", key)
|
||||
derived := sha256.Sum256([]byte("newapi-ciallo-v1:" + key))
|
||||
for _, tc := range []struct {
|
||||
token string
|
||||
code int
|
||||
}{{"", 401}, {key, 401}, {"incorrect", 401}, {fmt.Sprintf("%x", derived), 400}} {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("POST", "/api/bot/ciallo", strings.NewReader(`{}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request.Header.Set("Authorization", "Bearer "+tc.token)
|
||||
BotCiallo(c)
|
||||
assert.Equal(t, tc.code, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestChainDetectionMatrix(t *testing.T) {
|
||||
drivers := map[string]gorm.Dialector{"sqlite": sqlite.Open(":memory:")}
|
||||
if dsn := os.Getenv("TEST_MYSQL_DSN"); dsn != "" {
|
||||
drivers["mysql"] = mysql.Open(dsn)
|
||||
}
|
||||
if dsn := os.Getenv("TEST_POSTGRES_DSN"); dsn != "" {
|
||||
drivers["postgres"] = postgres.Open(dsn)
|
||||
}
|
||||
for name, driver := range drivers {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
db, err := gorm.Open(driver, &gorm.Config{NamingStrategy: schema.NamingStrategy{TablePrefix: fmt.Sprintf("chain_%d_", time.Now().UnixNano())}})
|
||||
require.NoError(t, err)
|
||||
conn, err := db.DB()
|
||||
require.NoError(t, err)
|
||||
conn.SetMaxOpenConns(1)
|
||||
old, redis := model.DB, common.RedisEnabled
|
||||
model.DB = db
|
||||
common.RedisEnabled = false
|
||||
t.Cleanup(func() {
|
||||
model.DB = old
|
||||
common.RedisEnabled = redis
|
||||
_ = db.Migrator().DropTable(&model.DetectionCall{}, &model.DetectionRun{}, &model.DetectionSchedule{}, &model.Channel{})
|
||||
_ = conn.Close()
|
||||
})
|
||||
require.NoError(t, db.AutoMigrate(&model.Channel{}, &model.DetectionRun{}, &model.DetectionCall{}, &model.DetectionSchedule{}))
|
||||
var count atomic.Int32
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
count.Add(1)
|
||||
assert.Equal(t, "Bearer test-only", r.Header.Get("Authorization"))
|
||||
raw, err := io.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
answer := strings.Repeat("42,", 300)
|
||||
prompt := gjson.GetBytes(raw, "input.0.content")
|
||||
if prompt.String() == "ciallo" {
|
||||
answer = "Ciallo~ hello"
|
||||
} else if prompt.IsArray() {
|
||||
w.WriteHeader(503)
|
||||
return
|
||||
}
|
||||
data, err := common.Marshal(map[string]any{"status": "completed", "output": []any{map[string]any{"type": "message", "content": []any{map[string]string{"type": "output_text", "text": answer}}}}})
|
||||
require.NoError(t, err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write(data)
|
||||
}))
|
||||
defer upstream.Close()
|
||||
url := upstream.URL
|
||||
ch := model.Channel{Type: 1, Key: "test-only", BaseURL: &url, Status: common.ChannelStatusEnabled, Models: fingerprint.Model, Group: "test group"}
|
||||
require.NoError(t, db.Create(&ch).Error)
|
||||
for _, kind := range []string{"vision", "ciallo"} {
|
||||
require.NoError(t, model.EnsureDetectionSchedule(context.Background(), model.DetectionSchedule{ChannelID: ch.Id, Kind: kind, IntervalMinutes: 45, NextDueAt: time.Now().Unix() + 2700}))
|
||||
}
|
||||
runs, err := enqueueChainTest(context.Background(), "test group", ch.Id)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, runs, 3)
|
||||
again, err := enqueueChainTest(context.Background(), "test group", ch.Id)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, strings.Split(runs[0].ID, "_")[0], strings.Split(again[0].ID, "_")[0])
|
||||
var wg sync.WaitGroup
|
||||
for _, run := range runs {
|
||||
wg.Add(1)
|
||||
go func() { defer wg.Done(); processDetection(context.Background(), run) }()
|
||||
}
|
||||
wg.Wait()
|
||||
require.NoError(t, db.Order("id asc").Find(&runs).Error)
|
||||
result, err := chainTestResult(runs, time.Now().Unix())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, result["done"])
|
||||
results := result["results"].(map[string]any)
|
||||
assert.Equal(t, "normal", results["ciallo"].(gin.H)["status"])
|
||||
assert.Equal(t, "failed", results["vision"].(gin.H)["status"])
|
||||
assert.NotEqual(t, "failed", results["fingerprint"].(gin.H)["status"])
|
||||
assert.Equal(t, int32(5), count.Load())
|
||||
for _, run := range runs {
|
||||
assert.Zero(t, run.Charged)
|
||||
assert.True(t, run.Settled)
|
||||
processDetection(context.Background(), run)
|
||||
}
|
||||
assert.Equal(t, int32(5), count.Load(), "completed calls must not replay")
|
||||
for _, kind := range []string{"vision", "ciallo"} {
|
||||
states, _, err := service.ReconcileAutomaticDetectionSchedules(context.Background(), []model.Channel{ch}, kind)
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, states[ch.Id].LastResultID, "manual chain must not move the automatic clock")
|
||||
}
|
||||
fresh, err := enqueueChainTest(context.Background(), "test group", ch.Id)
|
||||
require.NoError(t, err)
|
||||
assert.NotEqual(t, runs[0].ID, fresh[0].ID)
|
||||
result, err = chainTestResult(fresh, time.Now().Unix()+601)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, true, result["done"])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChainAuthorizationAndProjection(t *testing.T) {
|
||||
key := strings.Repeat("k", 32)
|
||||
t.Setenv("BOT_BALANCE_KEY", key)
|
||||
derived := sha256.Sum256([]byte("newapi-chaintest-v1:" + key))
|
||||
wrong := sha256.Sum256([]byte("newapi-ciallo-v1:" + key))
|
||||
for _, tc := range []struct {
|
||||
token string
|
||||
code int
|
||||
}{{"", 401}, {key, 401}, {fmt.Sprintf("%x", wrong), 401}, {fmt.Sprintf("%x", derived), 400}} {
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = httptest.NewRequest("POST", "/api/bot/chaintest", strings.NewReader(`{}`))
|
||||
c.Request.Header.Set("Content-Type", "application/json")
|
||||
c.Request.Header.Set("Authorization", "Bearer "+tc.token)
|
||||
BotChainTest(c)
|
||||
assert.Equal(t, tc.code, w.Code)
|
||||
}
|
||||
for _, status := range []string{"match", "mismatch", "inconclusive"} {
|
||||
encoded, _ := common.Marshal(fingerprint.Result{Engine: fingerprint.Version, Status: status, Closest: fingerprint.Model, ValidSamples: 3, Weight: 1, Gap: 1})
|
||||
runs := []model.DetectionRun{{ID: "abc_fingerprint", Kind: "fingerprint", Settled: true, Status: "succeeded", Result: string(encoded)}, {Kind: "ciallo", Settled: true, Status: "succeeded", Result: `{"closest":"Ciallo~"}`}, {Kind: "vision", Settled: true, Status: "succeeded", Result: `{"closest":"音箱"}`}}
|
||||
data, err := chainTestResult(runs, 0)
|
||||
require.NoError(t, err)
|
||||
results := data["results"].(map[string]any)
|
||||
assert.Equal(t, status, results["fingerprint"].(gin.H)["status"])
|
||||
assert.Equal(t, "normal", results["ciallo"].(gin.H)["status"])
|
||||
assert.Equal(t, "normal", results["vision"].(gin.H)["status"])
|
||||
raw, _ := common.Marshal(data)
|
||||
assert.NotContains(t, string(raw), "channel_id")
|
||||
assert.NotContains(t, string(raw), "price")
|
||||
runs[2].Status = "failed"
|
||||
runs[2].Error = "vision_keyword_missing"
|
||||
runs[2].Result = `{"closest":"我这边没有收到可查看的图片或附件"}`
|
||||
data, err = chainTestResult(runs, 0)
|
||||
require.NoError(t, err)
|
||||
results = data["results"].(map[string]any)
|
||||
assert.Equal(t, "incomplete", results["vision"].(gin.H)["status"])
|
||||
assert.Equal(t, status, results["fingerprint"].(gin.H)["status"])
|
||||
assert.Equal(t, "normal", results["ciallo"].(gin.H)["status"])
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user