Unify auth middleware and remove X-User-ID trust, add project_members authorization, sandbox workspaces via resolver, and admin-only reconcile by run_id. Introduce goose migrations with startup schema gate, fail closed when DB unavailable outside explicit ALLOW_IN_MEMORY, bundle web/dist in app Dockerfile, and remove frontend pseudo-login fallback.
128 lines
4.2 KiB
Go
128 lines
4.2 KiB
Go
package server_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/bizdev"
|
|
"chat-agent-system/internal/config"
|
|
"chat-agent-system/internal/server"
|
|
)
|
|
|
|
func TestServer_ChatRestoreAfterRestart(t *testing.T) {
|
|
cfg := &config.Config{
|
|
HTTPHost: "127.0.0.1",
|
|
HTTPPort: 8889,
|
|
DataDir: t.TempDir(),
|
|
}
|
|
svc := bizdev.MustNewService(t, cfg)
|
|
srv := server.NewServer(cfg, svc)
|
|
handler := srv.Routes()
|
|
|
|
sessID := "sess_restore_test_001"
|
|
userID := int64(88)
|
|
|
|
// Turn 1: 用户发送第一条消息
|
|
chatReq1 := map[string]any{
|
|
"session_id": sessID,
|
|
"user_id": userID,
|
|
"project_name": "高频超声手术刀系统",
|
|
"device_type": "超声手术设备",
|
|
"message": "你好,我们正在规划立项一款高频超声手术刀系统,预算200万",
|
|
}
|
|
bodyBytes1, _ := json.Marshal(chatReq1)
|
|
req1 := httptest.NewRequest("POST", "/api/v1/chat", bytes.NewReader(bodyBytes1))
|
|
req1.Header.Set("Content-Type", "application/json")
|
|
rr1 := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr1, req1)
|
|
|
|
if rr1.Code != http.StatusOK {
|
|
t.Fatalf("turn 1 failed with status %d: %s", rr1.Code, rr1.Body.String())
|
|
}
|
|
|
|
var resp1 map[string]any
|
|
if err := json.Unmarshal(rr1.Body.Bytes(), &resp1); err != nil {
|
|
t.Fatalf("unmarshal resp1 failed: %v", err)
|
|
}
|
|
reply1, _ := resp1["reply_text"].(string)
|
|
if reply1 == "" {
|
|
t.Fatalf("turn 1 reply text is empty")
|
|
}
|
|
t.Logf("✓ Turn 1 完成: %s", reply1)
|
|
|
|
// 验证消息已被持久化至 ContextManager
|
|
msgs, err := svc.ContextManager().GetChatHistory(context.Background(), sessID, 10, 0)
|
|
if err != nil || len(msgs) != 2 {
|
|
t.Fatalf("expected 2 messages in ContextManager, got %d (err: %v)", len(msgs), err)
|
|
}
|
|
|
|
// 模拟服务重启 / 内存淘汰 (G9 CleanStaleSessions 触发清空内存 map)
|
|
time.Sleep(5 * time.Millisecond)
|
|
cleaned := srv.CleanStaleSessions(1 * time.Millisecond)
|
|
if cleaned < 1 {
|
|
t.Fatalf("expected at least 1 session to be cleaned from memory, got %d", cleaned)
|
|
}
|
|
t.Logf("✓ 成功模拟会话从内存完全淘汰 (释放会话数=%d)", cleaned)
|
|
|
|
// Turn 2: 服务重启后,用户携相同 session_id 再次发起追问 (G3 自动恢复)
|
|
chatReq2 := map[string]any{
|
|
"session_id": sessID,
|
|
"user_id": userID,
|
|
"message": "关于刚才说的这个项目,国内型检和审评审批有哪些核心强标?",
|
|
}
|
|
bodyBytes2, _ := json.Marshal(chatReq2)
|
|
req2 := httptest.NewRequest("POST", "/api/v1/chat", bytes.NewReader(bodyBytes2))
|
|
req2.Header.Set("Content-Type", "application/json")
|
|
rr2 := httptest.NewRecorder()
|
|
handler.ServeHTTP(rr2, req2)
|
|
|
|
if rr2.Code != http.StatusOK {
|
|
t.Fatalf("turn 2 failed with status %d: %s", rr2.Code, rr2.Body.String())
|
|
}
|
|
|
|
var resp2 map[string]any
|
|
if err := json.Unmarshal(rr2.Body.Bytes(), &resp2); err != nil {
|
|
t.Fatalf("unmarshal resp2 failed: %v", err)
|
|
}
|
|
reply2, _ := resp2["reply_text"].(string)
|
|
if reply2 == "" {
|
|
t.Fatalf("turn 2 reply text is empty")
|
|
}
|
|
t.Logf("✓ Turn 2 完成: %s", reply2)
|
|
|
|
// 验证完整历史记录包含全部 4 条消息 (Turn 1 user+asst, Turn 2 user+asst)
|
|
allMsgs, err := svc.ContextManager().GetChatHistory(context.Background(), sessID, 10, 0)
|
|
if err != nil {
|
|
t.Fatalf("failed to query chat history: %v", err)
|
|
}
|
|
if len(allMsgs) != 4 {
|
|
t.Fatalf("expected 4 total messages after restore, got %d", len(allMsgs))
|
|
}
|
|
if allMsgs[0].Role != "user" || !strings.Contains(allMsgs[0].Content, "高频超声手术刀系统") {
|
|
t.Errorf("turn 1 user message mismatch: %+v", allMsgs[0])
|
|
}
|
|
if allMsgs[2].Role != "user" || !strings.Contains(allMsgs[2].Content, "核心强标") {
|
|
t.Errorf("turn 2 user message mismatch: %+v", allMsgs[2])
|
|
}
|
|
|
|
// 通过 /api/v1/projects 检查恢复后的上下文事实
|
|
reqCtx := httptest.NewRequest("GET", "/api/v1/projects?session_id="+sessID, nil)
|
|
rrCtx := httptest.NewRecorder()
|
|
handler.ServeHTTP(rrCtx, reqCtx)
|
|
if rrCtx.Code != http.StatusOK {
|
|
t.Fatalf("GET /api/v1/projects failed: %d", rrCtx.Code)
|
|
}
|
|
var ctxMap map[string]any
|
|
_ = json.Unmarshal(rrCtx.Body.Bytes(), &ctxMap)
|
|
facts, _ := ctxMap["blackboard_facts"].(map[string]any)
|
|
if facts != nil {
|
|
t.Logf("✓ 恢复后的项目黑板事实: %v", facts)
|
|
}
|
|
}
|