Files
Agent-System/internal/server/e2e_test.go
weichen 3145550a59 feat(security): complete P0 release blocking remediation
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.
2026-09-13 02:50:22 +00:00

312 lines
8.2 KiB
Go

package server
import (
"bufio"
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
"chat-agent-system/internal/stream"
)
func TestE2E_ChatSSEStreamPipeline(t *testing.T) {
cfg := &config.Config{
Env: "test",
AgentBackend: "stub",
DataDir: "./data/test",
KbDbPort: 5432,
}
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
// 1. 发起 SSE 流式对话请求
reqBody := StreamChatRequest{
SessionID: "e2e_sse_session_01",
Message: "查一下当前进展",
IncludeDetails: true,
ProjectName: "医用高清内窥镜系统",
CustomerName: "某三甲医院联合研发中心",
DeviceType: "内窥镜",
TargetMarket: "国内",
}
jsonBytes, _ := json.Marshal(reqBody)
req, err := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/chat/stream", bytes.NewReader(jsonBytes))
if err != nil {
t.Fatalf("create request failed: %v", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
t.Fatalf("do request failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected status 200, got %d", resp.StatusCode)
}
contentType := resp.Header.Get("Content-Type")
if !strings.Contains(contentType, "text/event-stream") {
t.Fatalf("expected Content-Type text/event-stream, got %s", contentType)
}
// 2. 流式解析读取
scanner := bufio.NewScanner(resp.Body)
var events []stream.StreamEvent
var currentEventType string
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "event: ") {
currentEventType = strings.TrimPrefix(line, "event: ")
} else if strings.HasPrefix(line, "data: ") {
dataStr := strings.TrimPrefix(line, "data: ")
var ev stream.StreamEvent
if err := json.Unmarshal([]byte(dataStr), &ev); err == nil {
if ev.Type == "" {
ev.Type = stream.EventType(currentEventType)
}
events = append(events, ev)
}
}
}
if len(events) == 0 {
t.Fatalf("expected streamed events, got 0")
}
// 3. 验证关键事件存在性与顺序
foundPhase := false
foundStatus := false
foundDone := false
for _, ev := range events {
switch ev.Type {
case stream.EventPhase:
foundPhase = true
if ev.Phase == nil || ev.Phase.Message == "" {
t.Errorf("expected non-empty phase message")
}
case stream.EventStatus:
foundStatus = true
if ev.Status == nil || ev.Status.Intent == "" {
t.Errorf("expected non-empty status payload")
}
case stream.EventDone:
foundDone = true
}
}
if !foundPhase {
t.Errorf("expected EventPhase in stream")
}
if !foundStatus {
t.Errorf("expected EventStatus in stream")
}
if !foundDone {
t.Errorf("expected EventDone in stream")
}
}
func TestE2E_TasksRunStreamPipeline(t *testing.T) {
cfg := &config.Config{
Env: "test",
AgentBackend: "stub",
DataDir: "./data/test",
KbDbPort: 5432,
}
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
reqBody := TaskRunRequestDTO{
TaskKey: "intake",
ProjectName: "超声内镜系统",
CustomerName: "北京水木东方",
DeviceType: "超声主机",
Backend: "stub",
}
jsonBytes, _ := json.Marshal(reqBody)
resp, err := http.Post(ts.URL+"/api/v1/tasks/run/stream", "application/json", bytes.NewReader(jsonBytes))
if err != nil {
t.Fatalf("post tasks run stream failed: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
t.Fatalf("expected status 200, got %d", resp.StatusCode)
}
scanner := bufio.NewScanner(resp.Body)
eventCount := 0
hasDeliverable := false
hasDone := false
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data: ") {
eventCount++
var ev stream.StreamEvent
dataStr := strings.TrimPrefix(line, "data: ")
if err := json.Unmarshal([]byte(dataStr), &ev); err == nil {
if ev.Type == stream.EventDeliverable {
hasDeliverable = true
}
if ev.Type == stream.EventDone {
hasDone = true
}
}
}
}
if eventCount == 0 {
t.Fatalf("expected task run events, got 0")
}
if !hasDeliverable {
t.Errorf("expected EventDeliverable in task stream")
}
if !hasDone {
t.Errorf("expected EventDone in task stream")
}
}
func TestE2E_ClaimedDocument_SSE_Interception(t *testing.T) {
cfg := &config.Config{
Env: "test",
AgentBackend: "stub",
DataDir: t.TempDir(),
KbDbPort: 5432,
AdminUsername: "admin",
AdminPassword: "admin-secret-password-123",
}
svc := bizdev.MustNewService(t, cfg)
srv := NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
projectID := int64(999)
authorID := int64(101)
// 1. 首次触发任务执行 (生成正式产物)
taskReq1 := TaskRunRequestDTO{
TaskKey: "feasibility_reg",
ProjectName: "智能微创手术机器人",
CustomerName: "北京某三甲医院",
DeviceType: "腔镜手术机器人",
Backend: "stub",
ProjectID: projectID,
AuthorID: authorID,
AuthorName: "张工",
}
reqBytes1, _ := json.Marshal(taskReq1)
res1, err := http.Post(ts.URL+"/api/v1/tasks/run", "application/json", bytes.NewReader(reqBytes1))
if err != nil || res1.StatusCode != http.StatusOK {
t.Fatalf("first task run failed: %v", err)
}
var taskRes1 bizdev.TaskRunResult
if err := json.NewDecoder(res1.Body).Decode(&taskRes1); err != nil {
t.Fatalf("decode first task run: %v", err)
}
res1.Body.Close()
docID := "doc_proj999_feasibility_reg"
if taskRes1.DocRecord != nil && taskRes1.DocRecord.DocID != "" {
docID = taskRes1.DocRecord.DocID
}
// 2. 编制人查阅正文触发认领
claimReq := DocGovClaimRequest{
DocID: docID,
UserID: authorID,
UserName: "张工",
UserRole: "author",
}
claimBytes, _ := json.Marshal(claimReq)
claimHTTP, _ := http.NewRequest(http.MethodPost, ts.URL+"/api/v1/docgov/claim", bytes.NewReader(claimBytes))
claimHTTP.Header.Set("Content-Type", "application/json")
withBearerAuth(claimHTTP, loginTestAdmin(t, srv))
resClaim, err := http.DefaultClient.Do(claimHTTP)
if err != nil || resClaim.StatusCode != http.StatusOK {
t.Fatalf("docgov claim failed: status=%v err=%v", func() int {
if resClaim != nil {
return resClaim.StatusCode
}
return 0
}(), err)
}
resClaim.Body.Close()
// 3. 已认领文档通过 SSE 流式触发再次执行 -> 验证拦截并推送 governance 与 summary 关键节点事件
taskReq2 := taskReq1
taskReq2.TargetMarket = "欧盟CE+美国FDA"
reqBytes2, _ := json.Marshal(taskReq2)
respStream, err := http.Post(ts.URL+"/api/v1/tasks/run/stream", "application/json", bytes.NewReader(reqBytes2))
if err != nil {
t.Fatalf("post tasks run stream failed: %v", err)
}
defer respStream.Body.Close()
if respStream.StatusCode != http.StatusOK {
t.Fatalf("expected status 200, got %d", respStream.StatusCode)
}
scanner := bufio.NewScanner(respStream.Body)
var events []stream.StreamEvent
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, "data: ") {
var ev stream.StreamEvent
dataStr := strings.TrimPrefix(line, "data: ")
if err := json.Unmarshal([]byte(dataStr), &ev); err == nil {
events = append(events, ev)
}
}
}
foundGovernance := false
foundSummary := false
var propID string
for _, ev := range events {
if ev.Type == stream.EventGovernance && ev.Governance != nil {
foundGovernance = true
if ev.Governance.Action == "proposal_required" && ev.Governance.AuthorClaimed {
propID = ev.Governance.ProposalID
t.Logf("✓ SSE 流中成功捕获文档治理拦截事件: proposal_id=%s, 简报=%s",
propID, ev.Governance.NotificationTitle)
}
}
if ev.Type == stream.EventSummary && ev.Summary != nil {
foundSummary = true
t.Logf("✓ SSE 流中成功捕获轻量级摘要事件: 标题=%s, 内容=%s",
ev.Summary.Title, ev.Summary.Content)
}
}
if !foundGovernance {
t.Errorf("expected EventGovernance in SSE stream for claimed document")
}
if !foundSummary {
t.Errorf("expected EventSummary in SSE stream for claimed document")
}
if propID == "" {
t.Errorf("expected valid proposal_id in governance event")
}
}