Files
Agent-System/internal/server/lease_concurrency_test.go
weichenandCursor 3f42149457 feat(bizdev): snapshot collaborators onto Runtime and Server ports
Composition roots copy Parts once so HTTP/CLI handlers stop calling locator getters on every request.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 13:00:33 +00:00

132 lines
4.4 KiB
Go

package server_test
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
"chat-agent-system/internal/server"
)
// TestDocEditLease_HTTPConcurrencyGates 验证:小队租约占用期间批注/二次租约/内容保存均返回 409
func TestDocEditLease_HTTPConcurrencyGates(t *testing.T) {
cfg := &config.Config{
AgentBackend: "stub",
DataDir: t.TempDir(),
AllowInMemory: true,
}
svc := bizdev.MustNewService(t, cfg)
srv := server.NewServer(cfg, svc)
ts := httptest.NewServer(srv.Routes())
defer ts.Close()
docID := fmt.Sprintf("doc_lease_http_concurrency_%d_a", time.Now().UnixNano())
ctx := t.Context()
_, err := bizdev.Snapshot(svc).Gov.CreateOrGetDocument(ctx, docID, 91001, "intake", "并发互斥测试文档", "V0.1", "",
1, "作者", 2, "审核", 3, "批准")
if err != nil {
t.Fatalf("create doc: %v", err)
}
// 1. 小队获取租约
acqBody, _ := json.Marshal(map[string]any{
"doc_id": docID, "holder_type": "squad", "holder_id": "run_test_1", "ttl_seconds": 600,
})
resp, err := http.Post(ts.URL+"/api/v1/docgov/lease/acquire", "application/json", bytes.NewReader(acqBody))
if err != nil {
t.Fatalf("acquire: %v", err)
}
if resp.StatusCode != http.StatusOK {
t.Fatalf("acquire status=%d", resp.StatusCode)
}
resp.Body.Close()
// 2. 人工二次租约应 409
humanAcq, _ := json.Marshal(map[string]any{
"doc_id": docID, "holder_type": "human", "holder_id": "1", "ttl_seconds": 60,
})
resp2, err := http.Post(ts.URL+"/api/v1/docgov/lease/acquire", "application/json", bytes.NewReader(humanAcq))
if err != nil {
t.Fatalf("human acquire: %v", err)
}
if resp2.StatusCode != http.StatusConflict {
t.Fatalf("expected 409 for second lease, got %d", resp2.StatusCode)
}
resp2.Body.Close()
// 3. 批注应 409
cmtBody, _ := json.Marshal(map[string]any{
"doc_id": docID, "user_id": 2, "user_name": "审核", "user_role": "reviewer",
"target_section": "## 结论", "content": "请补充数据",
})
resp3, err := http.Post(ts.URL+"/api/v1/docgov/comments", "application/json", bytes.NewReader(cmtBody))
if err != nil {
t.Fatalf("comment: %v", err)
}
if resp3.StatusCode != http.StatusConflict {
t.Fatalf("expected 409 for comment while leased, got %d", resp3.StatusCode)
}
resp3.Body.Close()
// 4. 内容保存应 409
saveBody, _ := json.Marshal(map[string]any{
"doc_id": docID, "user_id": 1, "user_name": "作者", "user_role": "author",
"markdown_body": "# 试图覆写\n",
})
resp4, err := http.Post(ts.URL+"/api/v1/docgov/content", "application/json", bytes.NewReader(saveBody))
if err != nil {
t.Fatalf("content: %v", err)
}
if resp4.StatusCode != http.StatusConflict {
t.Fatalf("expected 409 for content save while leased, got %d", resp4.StatusCode)
}
resp4.Body.Close()
// 5. 释放后批注应成功
relBody, _ := json.Marshal(map[string]any{
"doc_id": docID, "holder_type": "squad", "holder_id": "run_test_1",
})
resp5, err := http.Post(ts.URL+"/api/v1/docgov/lease/release", "application/json", bytes.NewReader(relBody))
if err != nil {
t.Fatalf("release: %v", err)
}
if resp5.StatusCode != http.StatusOK {
t.Fatalf("release status=%d", resp5.StatusCode)
}
resp5.Body.Close()
resp6, err := http.Post(ts.URL+"/api/v1/docgov/comments", "application/json", bytes.NewReader(cmtBody))
if err != nil {
t.Fatalf("comment after release: %v", err)
}
if resp6.StatusCode != http.StatusOK {
t.Fatalf("expected 200 after release, got %d", resp6.StatusCode)
}
resp6.Body.Close()
// 6. 不同文档可并行持有租约
doc2 := fmt.Sprintf("doc_lease_http_concurrency_%d_b", time.Now().UnixNano())
_, _ = bizdev.Snapshot(svc).Gov.CreateOrGetDocument(ctx, doc2, 91001, "research", "并行文档B", "V0.5", "",
1, "作者", 2, "审核", 3, "批准")
acqA, _ := json.Marshal(map[string]any{
"doc_id": docID, "holder_type": "squad", "holder_id": "run_a", "ttl_seconds": 60,
})
acqB, _ := json.Marshal(map[string]any{
"doc_id": doc2, "holder_type": "squad", "holder_id": "run_b", "ttl_seconds": 60,
})
rA, _ := http.Post(ts.URL+"/api/v1/docgov/lease/acquire", "application/json", bytes.NewReader(acqA))
rB, _ := http.Post(ts.URL+"/api/v1/docgov/lease/acquire", "application/json", bytes.NewReader(acqB))
if rA.StatusCode != 200 || rB.StatusCode != 200 {
t.Fatalf("parallel different docs should both acquire: %d %d", rA.StatusCode, rB.StatusCode)
}
rA.Body.Close()
rB.Body.Close()
}