AI Gateway Go 0.10.0 源码快照 + 旗舰版需求规划报告
M0-M7 已完成:核心网关(身份/RBAC/TOTP/OIDC/SAML/Provider/配额/路由/内容策略/审计/定价)+ 资源市场(MCP/Skills/数字员工)。 含 22 个 PostgreSQL 迁移、管理端/门户端前端源码、OpenAPI 契约、部署 compose。 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
package workbench
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"aigateway.local/core/internal/apikey"
|
||||
"aigateway.local/core/internal/gateway"
|
||||
"aigateway.local/core/internal/platform/config"
|
||||
"aigateway.local/core/internal/platform/cryptox"
|
||||
"aigateway.local/core/internal/platform/database"
|
||||
)
|
||||
|
||||
func TestWorkbenchPostgreSQLLifecycle(t *testing.T) {
|
||||
databaseURL := os.Getenv("WORKBENCH_TEST_DATABASE_URL")
|
||||
if databaseURL == "" {
|
||||
t.Skip("WORKBENCH_TEST_DATABASE_URL is not set")
|
||||
}
|
||||
ctx := context.Background()
|
||||
pool, err := database.Open(ctx, config.Database{URL: databaseURL, MaxConns: 8, MinConns: 0})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer pool.Close()
|
||||
actorID := "11111111-1111-4111-8111-111111111111"
|
||||
_, err = pool.Exec(ctx, `INSERT INTO gateway.admin_accounts(id,username,password_hash,role) VALUES($1,'m4-test','test','superadmin') ON CONFLICT(id) DO NOTHING`, actorID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cleanup := func() {
|
||||
_, _ = pool.Exec(ctx, `DELETE FROM gateway.notification_channels WHERE name='m4-webhook'; DELETE FROM gateway.applications WHERE code='m4_app'; DELETE FROM gateway.tool_definitions WHERE code='m4_lookup'; DELETE FROM gateway.knowledge_bases WHERE name='m4-integration-kb'; DELETE FROM gateway.prompt_templates WHERE name='m4-integration-prompt'`)
|
||||
}
|
||||
cleanup()
|
||||
defer cleanup()
|
||||
assets := NewService(pool)
|
||||
prompt, err := assets.CreatePrompt(ctx, PromptInput{Name: "m4-integration-prompt", Description: "test", Enabled: true, Content: "请回答 {{question}}", Variables: []Variable{{Name: "question", Required: true}}}, actorID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if prompt.Current == nil || prompt.Current.Version != 1 {
|
||||
t.Fatalf("unexpected prompt: %#v", prompt)
|
||||
}
|
||||
version, err := assets.AddPromptVersion(ctx, prompt.ID, "新版 {{question}}", []Variable{{Name: "question", Required: true}}, "v2", actorID, true)
|
||||
if err != nil || version.Version != 2 {
|
||||
t.Fatalf("version=%#v err=%v", version, err)
|
||||
}
|
||||
_, rendered, err := assets.RenderPromptByName(ctx, prompt.Name, map[string]any{"question": "可扩展吗"})
|
||||
if err != nil || rendered != "新版 可扩展吗" {
|
||||
t.Fatalf("render=%q err=%v", rendered, err)
|
||||
}
|
||||
kb, err := assets.SaveKnowledgeBase(ctx, KnowledgeBase{Name: "m4-integration-kb", Description: "test", RetrievalMode: "postgres_fts", ChunkSize: 200, ChunkOverlap: 20, Enabled: true}, actorID, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
doc, err := assets.AddKnowledgeDocument(ctx, kb.ID, "Go 架构说明", "text", "", "Go 网关采用不可变运行时快照。\n\nPostgreSQL 是权威配置存储。", actorID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if doc.ChunkCount == 0 {
|
||||
t.Fatal("expected chunks")
|
||||
}
|
||||
hits, err := NewPostgreSQLRetriever(assets).Search(ctx, kb.ID, "不可变运行时快照", 4)
|
||||
if err != nil || len(hits) == 0 {
|
||||
t.Fatalf("hits=%#v err=%v", hits, err)
|
||||
}
|
||||
key := base64.StdEncoding.EncodeToString(make([]byte, 32))
|
||||
cipher, err := cryptox.NewKeyring(key, 1, "", "m4-test-tools")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
receivedAuth := ""
|
||||
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
receivedAuth = r.Header.Get("Authorization")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
|
||||
}))
|
||||
defer upstream.Close()
|
||||
tools := NewToolService(assets, cipher, true)
|
||||
tool, err := tools.Save(ctx, "", ToolInput{Code: "m4_lookup", Name: "查询", EndpointURL: upstream.URL, HTTPMethod: "POST", Headers: map[string]string{"Authorization": "Bearer secret"}, InputSchema: json.RawMessage(`{"type":"object","required":["q"],"properties":{"q":{"type":"string"}}}`), TimeoutSeconds: 5, Enabled: true}, actorID, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
result, err := tools.Execute(ctx, tool, map[string]any{"q": "gateway"}, "", "m4-test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result["status_code"] != http.StatusOK || receivedAuth != "Bearer secret" {
|
||||
t.Fatalf("result=%#v auth=%q", result, receivedAuth)
|
||||
}
|
||||
app, err := assets.SaveApplication(ctx, Application{Code: "m4_app", Name: "M4 App", Status: "draft", DraftConfig: ApplicationConfig{Model: "test-model", PromptTemplateID: prompt.ID, KnowledgeBaseIDs: []string{kb.ID}, ToolIDs: []string{tool.ID}, RetrievalTopK: 4, Temperature: .2, MaxToolRounds: 2}}, actorID, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
published, err := assets.PublishApplication(ctx, app.ID, "first", actorID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if published.PublishedVersion == nil || *published.PublishedVersion != 1 || published.Status != "active" {
|
||||
t.Fatalf("unexpected published app: %#v", published)
|
||||
}
|
||||
governed := false
|
||||
fakeGateway := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var payload map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
messages, _ := payload["messages"].([]any)
|
||||
if len(messages) > 0 && strings.Contains(toString(messages[0]), "不可变运行时快照") {
|
||||
governed = true
|
||||
}
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"choices": []any{map[string]any{"message": map[string]any{"role": "assistant", "content": "完成"}}}})
|
||||
})
|
||||
runtime := NewRuntimeHTTPHandler(assets, tools, NewPostgreSQLRetriever(assets), staticPrincipalAuthenticator{}, fakeGateway, MarketplaceDeps{})
|
||||
runtimeRequest := httptest.NewRequest(http.MethodPost, "/v1/applications/m4_app/chat/completions", bytes.NewBufferString(`{"messages":[{"role":"user","content":"不可变运行时快照是什么?"}],"variables":{"question":"架构"}}`))
|
||||
runtimeRequest.Header.Set("Authorization", "Bearer test")
|
||||
runtimeRequest = runtimeRequest.WithContext(gateway.WithRequestID(runtimeRequest.Context(), "m4-runtime"))
|
||||
runtimeResponse := httptest.NewRecorder()
|
||||
runtime.ServeHTTP(runtimeResponse, runtimeRequest)
|
||||
if runtimeResponse.Code != http.StatusOK || !governed || !strings.Contains(runtimeResponse.Body.String(), `"application"`) {
|
||||
t.Fatalf("runtime status=%d governed=%v body=%s", runtimeResponse.Code, governed, runtimeResponse.Body.String())
|
||||
}
|
||||
|
||||
signed := ""
|
||||
webhook := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
signed = r.Header.Get("X-Gateway-Signature")
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}))
|
||||
defer webhook.Close()
|
||||
notifications := NewNotificationService(assets, cipher, true)
|
||||
secret := "m4-signing-secret"
|
||||
channel, err := notifications.SaveChannel(ctx, "", NotificationInput{Name: "m4-webhook", WebhookURL: webhook.URL, SigningSecret: &secret, EventPatterns: []string{"application.*"}, Enabled: true}, actorID, true)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
eventID, _ := newUUID()
|
||||
delivery, err := notifications.ensureDelivery(ctx, channel, eventID, "application.published", json.RawMessage(`{"version":1}`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = notifications.deliver(ctx, channel, delivery); err != nil || !strings.HasPrefix(signed, "sha256=") {
|
||||
t.Fatalf("delivery err=%v signature=%q", err, signed)
|
||||
}
|
||||
}
|
||||
|
||||
type staticPrincipalAuthenticator struct{}
|
||||
|
||||
func (staticPrincipalAuthenticator) AuthenticatePrincipal(context.Context, string) (apikey.Principal, error) {
|
||||
return apikey.Principal{}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user