Resolve conflicts: both CURRENT claim tables (slot preserved), local multi-host coordination plan + origin phased companion, origin module-boundaries, combined PG isolation with remote product tests. FEATURE-MATRIX takes origin record_id layout; A-G done bump deferred pending evidence row apply.
168 lines
7.0 KiB
Go
168 lines
7.0 KiB
Go
package server_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"chat-agent-system/internal/bizdev"
|
|
"chat-agent-system/internal/ctxmgr"
|
|
"chat-agent-system/internal/projectinstance"
|
|
"chat-agent-system/internal/workflowdef"
|
|
)
|
|
|
|
func TestChatHistoryRefresh_PG_PreActivationAndTaskScope(t *testing.T) {
|
|
pool := openPGPoolWithMigrations(t)
|
|
if pool == nil {
|
|
return
|
|
}
|
|
defer pool.Close()
|
|
|
|
ctx := context.Background()
|
|
handler, srv := setupPGGovernanceServer(t, pool)
|
|
ctxMgr := bizdev.Snapshot(srv.BizdevService()).Ctx
|
|
if ctxMgr == nil {
|
|
t.Fatal("context manager required")
|
|
}
|
|
|
|
wfKey := workflowdef.OfficialKeyMain
|
|
seedOfficialWorkflowManifest(t, pool, wfKey)
|
|
|
|
creatorToken := loginTestUser(t, handler, "zhanggong", "123456")
|
|
creatorID := fetchAuthUserID(t, handler, creatorToken)
|
|
|
|
var projectID int64
|
|
code := "pg-refresh-" + time.Now().Format("150405.000")
|
|
if err := pool.QueryRow(ctx, `
|
|
INSERT INTO projects (code, name, status, creator_user_id, lifecycle_status, active_workflow_key, workflow_selection_version, workflow_plan_revision)
|
|
VALUES ($1, $2, 'active', $3, 'planning', '', 1, 0) RETURNING id
|
|
`, code, "PG Refresh Test "+code, creatorID).Scan(&projectID); err != nil {
|
|
t.Fatalf("insert project: %v", err)
|
|
}
|
|
_, _ = pool.Exec(ctx, `
|
|
INSERT INTO project_members (project_id, user_id, project_role, status, joined_at, updated_at)
|
|
VALUES ($1, $2, 'owner', 'active', now(), now()) ON CONFLICT DO NOTHING
|
|
`, projectID, creatorID)
|
|
t.Cleanup(func() { cleanupProject(t, pool, projectID) })
|
|
|
|
sessID := fmt.Sprintf("sess-p%d-u%d", projectID, creatorID)
|
|
_, _ = ctxMgr.EnsureChatSession(ctx, sessID, creatorID, projectID, "Refresh Test")
|
|
|
|
preMeta := map[string]any{"project_id": projectID}
|
|
projectinstance.ApplyScopeToMetadata(preMeta, &projectinstance.MessageScope{ScopeKind: projectinstance.ScopePreActivation})
|
|
if _, err := ctxMgr.RecordUserMessageWithID(ctx, sessID, creatorID, "user", "激活前需求沟通", "SUPPLEMENT_INFO", "", preMeta); err != nil {
|
|
t.Fatalf("record pre-activation: %v", err)
|
|
}
|
|
|
|
reqDefault := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/projects/%d/workflow-plan/default?workflow_key=%s&version=0", projectID, wfKey), nil)
|
|
withBearerAuth(reqDefault, creatorToken)
|
|
recDefault := httptest.NewRecorder()
|
|
handler.ServeHTTP(recDefault, reqDefault)
|
|
if recDefault.Code != http.StatusOK {
|
|
t.Fatalf("default plan: %d %s", recDefault.Code, recDefault.Body.String())
|
|
}
|
|
var plan projectinstance.WorkflowPlan
|
|
_ = json.Unmarshal(recDefault.Body.Bytes(), &plan)
|
|
reviewerID := fetchAuthUserID(t, handler, loginTestUser(t, handler, "lizhuguan", "123456"))
|
|
approverID := fetchAuthUserID(t, handler, loginTestUser(t, handler, "wangzong", "123456"))
|
|
// 治理校验要求审核/批准负责人为在任项目成员(DUTY_ASSIGNEE_NOT_ACTIVE_MEMBER)。
|
|
ensureProjectMembers(t, pool, projectID, map[int64]string{
|
|
creatorID: "owner",
|
|
reviewerID: "reviewer",
|
|
approverID: "approver",
|
|
})
|
|
for i := range plan.Stages {
|
|
for j := range plan.Stages[i].DocTasks {
|
|
plan.Stages[i].DocTasks[j].Duties = projectinstance.DocDuty{
|
|
AuthorUserID: creatorID, ReviewerUserID: reviewerID, ApproverUserID: approverID,
|
|
}
|
|
}
|
|
}
|
|
saveBody, _ := json.Marshal(projectinstance.SavePlanInput{Plan: plan, ExpectedPlanRevision: 0, ExpectedSelectionVersion: 1})
|
|
reqSave := httptest.NewRequest(http.MethodPut, fmt.Sprintf("/api/v1/projects/%d/workflow-plan", projectID), bytes.NewReader(saveBody))
|
|
withBearerAuth(reqSave, creatorToken)
|
|
recSave := httptest.NewRecorder()
|
|
handler.ServeHTTP(recSave, reqSave)
|
|
if recSave.Code != http.StatusOK {
|
|
t.Fatalf("save plan: %d %s", recSave.Code, recSave.Body.String())
|
|
}
|
|
|
|
reqPreview := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/activation-preview", projectID), nil)
|
|
withBearerAuth(reqPreview, creatorToken)
|
|
recPreview := httptest.NewRecorder()
|
|
handler.ServeHTTP(recPreview, reqPreview)
|
|
if recPreview.Code != http.StatusOK {
|
|
t.Fatalf("activation preview: %d %s", recPreview.Code, recPreview.Body.String())
|
|
}
|
|
var preview projectinstance.ActivationPreview
|
|
_ = json.Unmarshal(recPreview.Body.Bytes(), &preview)
|
|
activateBody, _ := json.Marshal(projectinstance.ActivateInput{
|
|
ConfirmationToken: preview.ConfirmationToken, ExpectedProjectVersion: preview.ExpectedProjectVersion,
|
|
ExpectedPlanRevision: preview.ExpectedPlanRevision,
|
|
})
|
|
reqActivate := httptest.NewRequest(http.MethodPost, fmt.Sprintf("/api/v1/projects/%d/workflow-instances/activate", projectID), bytes.NewReader(activateBody))
|
|
withBearerAuth(reqActivate, creatorToken)
|
|
recActivate := httptest.NewRecorder()
|
|
handler.ServeHTTP(recActivate, reqActivate)
|
|
if recActivate.Code != http.StatusOK {
|
|
t.Fatalf("activate: %d %s", recActivate.Code, recActivate.Body.String())
|
|
}
|
|
|
|
instSvc := projectinstance.NewService(projectinstance.NewStore(pool), nil, nil, nil)
|
|
taskScope, err := instSvc.ResolveMessageScopeForTask(ctx, projectID, "bd_intake")
|
|
if err != nil {
|
|
t.Fatalf("resolve task scope: %v", err)
|
|
}
|
|
taskUserMeta := map[string]any{"project_id": projectID, "task_key": "bd_intake", "client_turn_id": "turn-doc-1"}
|
|
projectinstance.ApplyScopeToMetadata(taskUserMeta, taskScope)
|
|
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, creatorID, "user", "请生成 intake 文档", "TRIGGER_TASK", "", taskUserMeta)
|
|
taskAsstMeta := map[string]any{"project_id": projectID, "task_key": "bd_intake", "task_status": "completed"}
|
|
projectinstance.ApplyScopeToMetadata(taskAsstMeta, taskScope)
|
|
_, _ = ctxMgr.RecordUserMessageWithID(ctx, sessID, 0, "assistant", "文档已生成", "TRIGGER_TASK", "", taskAsstMeta)
|
|
|
|
reqWindow := httptest.NewRequest(http.MethodGet, "/api/v1/chat/messages/window?session_id="+sessID+"&days=7", nil)
|
|
withBearerAuth(reqWindow, creatorToken)
|
|
recWindow := httptest.NewRecorder()
|
|
handler.ServeHTTP(recWindow, reqWindow)
|
|
if recWindow.Code != http.StatusOK {
|
|
t.Fatalf("window: %d %s", recWindow.Code, recWindow.Body.String())
|
|
}
|
|
var win ctxmgr.ChatWindowResult
|
|
_ = json.Unmarshal(recWindow.Body.Bytes(), &win)
|
|
totalMsgs := 0
|
|
for _, d := range win.Days {
|
|
totalMsgs += len(d.Messages)
|
|
}
|
|
if totalMsgs != 3 {
|
|
t.Fatalf("expected 3 visible messages after refresh, got %d", totalMsgs)
|
|
}
|
|
|
|
reqCat := httptest.NewRequest(http.MethodGet, "/api/v1/chat/categories?session_id="+sessID, nil)
|
|
withBearerAuth(reqCat, creatorToken)
|
|
recCat := httptest.NewRecorder()
|
|
handler.ServeHTTP(recCat, reqCat)
|
|
var cats []ctxmgr.ChatCategoryStat
|
|
_ = json.Unmarshal(recCat.Body.Bytes(), &cats)
|
|
catTotal := 0
|
|
for _, c := range cats {
|
|
catTotal += c.Count
|
|
}
|
|
if catTotal != 3 {
|
|
t.Fatalf("expected category count 3, got %d", catTotal)
|
|
}
|
|
|
|
guestToken := loginTestUser(t, handler, "guest", "123456")
|
|
reqForbidden := httptest.NewRequest(http.MethodGet, "/api/v1/chat/messages/window?session_id="+sessID+"&days=7", nil)
|
|
withBearerAuth(reqForbidden, guestToken)
|
|
recForbidden := httptest.NewRecorder()
|
|
handler.ServeHTTP(recForbidden, reqForbidden)
|
|
if recForbidden.Code != http.StatusForbidden {
|
|
t.Fatalf("expected 403 for non-owner, got %d %s", recForbidden.Code, recForbidden.Body.String())
|
|
}
|
|
}
|