Files
Agent-System/internal/server/task_run_persistence_pg_test.go
weichenandCursor aea391cdaf feat(taskrun): persist runs/events and fix finalize-before-done ordering
Add goose v19 task_runs/task_run_events tables with PG store for restart
replay, orphan interruption on startup, strict /ready checks, and strip
client work_dir from chat/SSE paths per P0 path sandbox requirements.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:23 +00:00

151 lines
4.1 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)
})
record := &taskrun.RunRecord{
RunID: runID, SessionID: "sess_pg_persist", ProjectID: 0,
UserID: fetchAuthUserID(t, handler, token), 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(), srv.BizdevService().ContextManager())
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"])
}
}