package server import ( "bytes" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "chat-agent-system/internal/docgov" "chat-agent-system/internal/projectinstance" ) func TestCreateTaskRunBusyPreflightDoesNotPersistRunOrMessages(t *testing.T) { srv := newTestServer(t) ctx := t.Context() docID := "doc_proj143_intake" if _, err := srv.svc.DocGov().CreateOrGetDocument( ctx, docID, 143, "intake", "用户初始需求收集表", "V0.1_triage", "", 1, "编制人", 1, "审核人", 1, "批准人", ); err != nil { t.Fatalf("create document: %v", err) } if _, err := srv.svc.DocGov().AcquireLease(ctx, docgov.LeaseAcquireRequest{ DocID: docID, HolderType: docgov.LeaseHolderHuman, HolderID: "editor-9", TTL: time.Minute, }); err != nil { t.Fatalf("acquire fixture lease: %v", err) } defer func() { _, _ = srv.svc.DocGov().ReleaseLease(t.Context(), docID, string(docgov.LeaseHolderHuman), "editor-9") }() requestBody, _ := json.Marshal(createTaskRunRequest{ TaskRunRequestDTO: TaskRunRequestDTO{TaskKey: "intake", ProjectID: 143, ProjectName: "隔离项目"}, SessionID: "fixture-busy-preflight", ClientTurnID: "turn-busy-1", UserMessage: "开始编写用户初始需求表", }) req := httptest.NewRequest(http.MethodPost, "/api/v1/tasks/runs", bytes.NewReader(requestBody)) withBearerAuth(req, loginTestAdmin(t, srv)) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code != http.StatusConflict { t.Fatalf("status=%d body=%s", w.Code, w.Body.String()) } var payload map[string]any if err := json.NewDecoder(w.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } if payload["error_code"] != "DOC_BUSY" { t.Fatalf("payload=%+v", payload) } if _, exists := payload["run_id"]; exists { t.Fatalf("busy preflight must not return run_id: %+v", payload) } history, err := srv.svc.ContextManager().GetChatHistory(ctx, "fixture-busy-preflight", 20, 0) if err != nil { t.Fatalf("get busy history: %v", err) } if len(history) != 2 { t.Fatalf("busy preflight history=%d want user+assistant", len(history)) } if history[1].Metadata["task_status"] != "failed" { t.Fatalf("assistant metadata=%+v", history[1].Metadata) } errorInfo, ok := history[1].Metadata["error_info"].(map[string]any) if !ok || errorInfo["code"] != "DOC_BUSY" { t.Fatalf("error_info=%+v", history[1].Metadata["error_info"]) } if actions, ok := errorInfo["actions"].([]string); !ok || len(actions) != 0 { t.Fatalf("busy actions=%+v", errorInfo["actions"]) } } // 从未激活项目:任务作用域为 pre_activation,创建 run 必须 202 而非 TASK_SCOPE_UNRESOLVED 409。 func TestCreateTaskRun_PreActivationAcceptedNot409(t *testing.T) { srv := newTestServer(t) store := projectinstance.NewMemoryStore() _ = projectinstance.SeedProjectMeta(store, 9001, projectinstance.LifecyclePlanning, "", 1, 0) srv.instSvc = projectinstance.NewService(store, projectinstance.TestResolver(), nil, nil) requestBody, _ := json.Marshal(createTaskRunRequest{ TaskRunRequestDTO: TaskRunRequestDTO{TaskKey: "intake", ProjectID: 9001, ProjectName: "预激活立项"}, SessionID: "sess-p9001-u1", ClientTurnID: "turn-preact-1", UserMessage: "请生成首轮摸底文档", }) req := httptest.NewRequest(http.MethodPost, "/api/v1/tasks/runs", bytes.NewReader(requestBody)) withBearerAuth(req, loginTestAdmin(t, srv)) w := httptest.NewRecorder() srv.Routes().ServeHTTP(w, req) if w.Code == http.StatusConflict { var payload map[string]any _ = json.NewDecoder(w.Body).Decode(&payload) if payload["error_code"] == "TASK_SCOPE_UNRESOLVED" { t.Fatalf("never-activated must not 409 TASK_SCOPE_UNRESOLVED: %+v", payload) } t.Fatalf("unexpected conflict: %+v", payload) } if w.Code != http.StatusAccepted { t.Fatalf("want 202 Accepted, got %d body=%s", w.Code, w.Body.String()) } }