Files
Agent-System/internal/server/task_run_persistence_pg_test.go
T
weichen 70882918e5 test(pg): isolate PG integration tests from shared dev database
修复 go test -short 在设置 TEST_DATABASE_URL 时的既有欠债:
- storage/notification/docgov:移除硬编码 agent_system_dev 回退 DSN,改为
  无 TEST_DATABASE_URL 即 skip;固定项目名改为 name||code 唯一化。
- docgov engine_db:改为隔离库 + 自建项目行,替换随机 projectID 撞外键的旧写法。
- server/*_pg、projectinstance/pg_integration:迁移到官方 main 工作流
  (BuildDefaultPlan official-only 治理后的正确前提),补齐在任成员
  (DUTY_ASSIGNEE_NOT_ACTIVE_MEMBER)、任务键 bd_intake、阶段键 V0.1_triage、
  项目作用域 run record(events.ValidateEnvelope 拒绝 project<=0)、
  task_run_resume 去除内嵌 dev 凭证的 config 硬编码。
- 新增 seedProjectForRun 测试辅助。
验证:fresh 隔离库(55432)上 go test -short -p 1 ./... 除 archtest
(另行注册表分支修复)外全部通过;包内并行迁移竞争需 -p 1。
2026-09-22 01:10:17 +00:00

153 lines
4.2 KiB
Go

package server_test
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/stream"
"chat-agent-system/internal/taskrun"
)
func TestTaskRun_PersistenceAndReplayAfterRestart_PG(t *testing.T) {
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
if !taskrun.TablesReady(context.Background(), pool) {
t.Skip("task_runs table not ready")
}
handler, srv := setupPGGovernanceServer(t, pool)
token := loginTestUser(t, handler, "zhanggong", "123456")
runID := "run_pg_persist_" + time.Now().Format("150405")
store := taskrun.NewPGStore(pool)
srv.TaskRuns().SetStore(store)
srv.TaskRuns().SetExecutor(
func(_ context.Context, _ string, _ bizdev.TaskRunRequest, st *stream.Streamer) (*bizdev.TaskRunResult, error) {
if st != nil {
st.EmitPhase("test", "running", "step1", 1, 1, 50)
}
return &bizdev.TaskRunResult{Success: true, WorkDir: t.TempDir(), TaskKey: "intake"}, nil
},
nil,
nil,
)
srv.TaskRuns().SetFinalizer(func(ctx context.Context, record *taskrun.RunRecord, result *bizdev.TaskRunResult) (string, error) {
return srv.FinalizeTaskRunProjection(ctx, record, result)
})
userID := fetchAuthUserID(t, handler, token)
projectID := seedProjectForRun(t, pool, "run-persist", userID)
record := &taskrun.RunRecord{
RunID: runID, SessionID: "sess_pg_persist", ProjectID: projectID,
UserID: userID, TaskKey: "intake", Status: "running",
}
_, err := srv.TaskRuns().CreateAndStart(context.Background(), runID, record, bizdev.TaskRunRequest{TaskKey: "intake"}, bizdev.StageRunRequest{}, "")
if err != nil {
t.Fatalf("CreateAndStart: %v", err)
}
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
rec, getErr := store.GetRun(context.Background(), runID)
if getErr == nil && rec.Status == "completed" {
break
}
time.Sleep(50 * time.Millisecond)
}
persisted, err := store.GetRun(context.Background(), runID)
if err != nil {
t.Fatalf("GetRun from store: %v", err)
}
if persisted.Status != "completed" {
t.Fatalf("expected completed, got %s", persisted.Status)
}
events, err := store.ListEvents(context.Background(), runID, 0)
if err != nil {
t.Fatalf("ListEvents: %v", err)
}
if len(events) == 0 {
t.Fatal("expected persisted events")
}
hasDone := false
for _, ev := range events {
if ev.Event.Type == stream.EventDone {
hasDone = true
}
}
if !hasDone {
t.Fatal("expected done event in persisted events")
}
// 模拟进程重启:新 Manager 仅挂载 Store
restarted := taskrun.NewManager(srv.ProjectWorkflowService(), bizdev.Snapshot(srv.BizdevService()).Ctx)
restarted.SetStore(store)
reloaded, err := restarted.GetRun(runID)
if err != nil {
t.Fatalf("GetRun after restart: %v", err)
}
if reloaded.Status != "completed" {
t.Fatalf("reloaded status=%s", reloaded.Status)
}
ch, replay, err := restarted.Subscribe(runID, 0)
if err != nil {
t.Fatalf("Subscribe after restart: %v", err)
}
if len(replay) == 0 {
t.Fatal("expected replay events from DB")
}
_, ok := <-ch
if ok {
t.Fatal("replay-only channel should be closed")
}
// HTTP GET 鉴权 + 查询
req := httptest.NewRequest(http.MethodGet, "/api/v1/tasks/runs/"+runID, nil)
withBearerAuth(req, token)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("GET task run: %d %s", rec.Code, rec.Body.String())
}
var body taskrun.RunRecord
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("decode: %v", err)
}
if body.RunID != runID {
t.Fatalf("run_id=%s", body.RunID)
}
}
func TestReadiness_StrictWhenPGConfigured(t *testing.T) {
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
handler, _ := setupPGGovernanceServer(t, pool)
req := httptest.NewRequest(http.MethodGet, "/api/v1/ready", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("ready expected 200, got %d %s", rec.Code, rec.Body.String())
}
var resp map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &resp)
if resp["task_store_ready"] != true {
t.Fatalf("task_store_ready=%v", resp["task_store_ready"])
}
}