Files
Agent-System/internal/taskrun/finalize_pg_test.go
T
weichenandCursor 3997a04bdb test(taskrun): cover duplicate finalize, cancel race, and SSE
Use an isolated dual-pool database so idempotent finalize, cancel CAS, commit-fail SSE, and receipt-gated cleanup are proven without a process mutex.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 02:06:21 +00:00

364 lines
13 KiB
Go

package taskrun_test
import (
"context"
"encoding/json"
"errors"
"net/url"
"os"
"path/filepath"
"sync"
"testing"
"time"
"chat-agent-system/internal/app/uow"
"chat-agent-system/internal/artifactvault"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/ctxmgr"
"chat-agent-system/internal/db"
"chat-agent-system/internal/docgov"
"chat-agent-system/internal/stream"
"chat-agent-system/internal/taskrun"
"chat-agent-system/internal/workspace"
"github.com/jackc/pgx/v5/pgxpool"
)
func init() {
if _, err := os.Stat("deploy/postgres/goose"); err == nil {
return
}
for _, cand := range []string{"../..", "../../.."} {
if fi, err := os.Stat(cand + "/deploy/postgres/goose"); err == nil && fi.IsDir() {
_ = os.Chdir(cand)
return
}
}
}
var (
isoCleanup func()
)
func TestMain(m *testing.M) {
code := m.Run()
if isoCleanup != nil {
isoCleanup()
}
os.Exit(code)
}
func rewriteDatabaseName(dsn, name string) string {
if u, err := url.Parse(dsn); err == nil && u.Scheme != "" {
u.Path = "/" + name
return u.String()
}
return dsn
}
var isoOnce sync.Once
var isoPoolA, isoPoolB *pgxpool.Pool
var isoErr error
func openIsolatedM3(t *testing.T) (*pgxpool.Pool, *pgxpool.Pool) {
t.Helper()
if testing.Short() {
t.Skip("U8 PG races are not part of -short")
}
dsn := db.SkipIfNoTestDatabaseURL(t)
db.RefuseSharedEnvDSN(t, dsn)
isoOnce.Do(func() {
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
defer cancel()
adminDSN := rewriteDatabaseName(dsn, "postgres")
admin, err := pgxpool.New(ctx, adminDSN)
if err != nil {
isoErr = err
return
}
const isolated = "agent_rel_m3_u8"
_, _ = admin.Exec(ctx, `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname=$1 AND pid <> pg_backend_pid()`, isolated)
_, _ = admin.Exec(ctx, `DROP DATABASE IF EXISTS `+isolated)
if _, err := admin.Exec(ctx, `CREATE DATABASE `+isolated); err != nil {
admin.Close()
isoErr = err
return
}
isoDSN := rewriteDatabaseName(dsn, isolated)
poolA, err := pgxpool.New(ctx, isoDSN)
if err != nil {
admin.Close()
isoErr = err
return
}
if err := db.RunMigrations(ctx, poolA, db.ResolveMigrationsDir()); err != nil {
poolA.Close()
admin.Close()
isoErr = err
return
}
poolB, err := pgxpool.New(ctx, isoDSN)
if err != nil {
poolA.Close()
admin.Close()
isoErr = err
return
}
isoPoolA, isoPoolB = poolA, poolB
isoCleanup = func() {
poolA.Close()
poolB.Close()
_, _ = admin.Exec(context.Background(), `SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname=$1 AND pid <> pg_backend_pid()`, isolated)
_, _ = admin.Exec(context.Background(), `DROP DATABASE IF EXISTS `+isolated)
admin.Close()
}
})
if isoErr != nil {
t.Fatalf("isolated db: %v", isoErr)
}
return isoPoolA, isoPoolB
}
func seedUserAndProject(t *testing.T, pool *pgxpool.Pool) (userID, projectID int64) {
t.Helper()
ctx := context.Background()
err := pool.QueryRow(ctx, `SELECT id FROM users ORDER BY id LIMIT 1`).Scan(&userID)
if err != nil {
t.Fatalf("seed user: %v", err)
}
err = pool.QueryRow(ctx, `SELECT id FROM projects ORDER BY id LIMIT 1`).Scan(&projectID)
if err != nil {
t.Fatalf("seed project: %v", err)
}
return userID, projectID
}
func insertRunning(t *testing.T, pool *pgxpool.Pool, rec *taskrun.RunRecord) {
t.Helper()
_, err := pool.Exec(context.Background(), `
INSERT INTO task_runs (
run_id, session_id, project_id, user_id, client_turn_id, task_key, status,
workspace_id, assistant_message_id, created_at, updated_at
) VALUES ($1,$2,$3,$4,$5,$6,'running',$7,$8,now(),now())
`, rec.RunID, rec.SessionID, rec.ProjectID, rec.UserID, rec.ClientTurnID, rec.TaskKey, rec.WorkspaceID, rec.AssistantMsgID)
if err != nil {
t.Fatalf("insert run: %v", err)
}
}
func TestPG_DuplicateFinalizeIdempotent(t *testing.T) {
poolA, poolB := openIsolatedM3(t)
userID, projectID := seedUserAndProject(t, poolA)
ctxMgr := ctxmgr.NewContextManager(poolA, nil, ctxmgr.TimeDecayConfig{})
sessionID := "sess_u8_dup"
asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), sessionID, userID, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{"project_id": projectID})
if err != nil {
t.Fatal(err)
}
rec := &taskrun.RunRecord{
RunID: "run_u8_dup", SessionID: sessionID, ProjectID: projectID, UserID: userID,
TaskKey: "intake", AssistantMsgID: asstID, WorkspaceID: "ws_u8_dup",
}
insertRunning(t, poolA, rec)
body := []byte("# committed intake body\n")
res := &bizdev.TaskRunResult{
Success: true, TaskKey: "intake", ArtifactTitle: "用户初始需求收集表", MarkdownBody: string(body),
DocRecord: &docgov.DocumentRecord{DocID: "doc_u8_dup", ContentSHA256: artifactvault.SHA256Hex(body), Version: 1, ProjectID: projectID},
}
cmdsA := &taskrun.Commands{UoW: uow.NewPG(poolA), Ctx: ctxMgr, Fav: &artifactvault.Ingest{Env: "dev"}, Env: "dev", StorageIdentity: "env:dev"}
cmdsB := &taskrun.Commands{UoW: uow.NewPG(poolB), Ctx: ctxmgr.NewContextManager(poolB, nil, ctxmgr.TimeDecayConfig{}), Fav: &artifactvault.Ingest{Env: "dev"}, Env: "dev", StorageIdentity: "env:dev"}
st1, err := cmdsA.Finalize(context.Background(), rec, res)
if err != nil {
t.Fatal(err)
}
st2, err := cmdsB.Finalize(context.Background(), rec, res)
if err != nil {
t.Fatal(err)
}
if st1 != "completed" || st2 != "completed" {
t.Fatalf("status %s %s", st1, st2)
}
var msgs, mans, envs, stgs int
_ = poolA.QueryRow(context.Background(), `SELECT COUNT(*) FROM chat_messages WHERE session_id=$1 AND role='assistant'`, sessionID).Scan(&msgs)
_ = poolA.QueryRow(context.Background(), `SELECT COUNT(*) FROM task_run_manifests WHERE run_id=$1`, rec.RunID).Scan(&mans)
_ = poolA.QueryRow(context.Background(), `SELECT COUNT(*) FROM event_envelopes WHERE aggregate_id=$1`, rec.RunID).Scan(&envs)
_ = poolA.QueryRow(context.Background(), `SELECT COUNT(*) FROM artifact_payload_staging WHERE source_event_id=$1`, "taskrun:"+rec.RunID+":completed").Scan(&stgs)
if msgs != 1 || mans != 1 || envs != 1 || stgs != 1 {
t.Fatalf("counts messages=%d manifests=%d envelopes=%d staging=%d", msgs, mans, envs, stgs)
}
var sha1, sha2 string
_ = poolA.QueryRow(context.Background(), `SELECT sha256 FROM task_run_manifests WHERE run_id=$1`, rec.RunID).Scan(&sha1)
_ = poolB.QueryRow(context.Background(), `SELECT sha256 FROM task_run_manifests WHERE run_id=$1`, rec.RunID).Scan(&sha2)
if sha1 == "" || sha1 != sha2 {
t.Fatalf("manifest sha %q %q", sha1, sha2)
}
}
func TestPG_FinalizeCancelRaceOneTerminal(t *testing.T) {
poolA, poolB := openIsolatedM3(t)
userID, projectID := seedUserAndProject(t, poolA)
ctxMgr := ctxmgr.NewContextManager(poolA, nil, ctxmgr.TimeDecayConfig{})
sessionID := "sess_u8_race"
asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), sessionID, userID, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{"project_id": projectID})
if err != nil {
t.Fatal(err)
}
rec := &taskrun.RunRecord{
RunID: "run_u8_race", SessionID: sessionID, ProjectID: projectID, UserID: userID,
TaskKey: "intake", AssistantMsgID: asstID, WorkspaceID: "ws_u8_race",
}
insertRunning(t, poolA, rec)
body := []byte("# race body\n")
res := &bizdev.TaskRunResult{
Success: true, TaskKey: "intake", MarkdownBody: string(body),
DocRecord: &docgov.DocumentRecord{DocID: "doc_u8_race", ContentSHA256: artifactvault.SHA256Hex(body), Version: 1},
}
cmdsA := &taskrun.Commands{UoW: uow.NewPG(poolA), Ctx: ctxMgr, Fav: &artifactvault.Ingest{Env: "dev"}, Env: "dev", StorageIdentity: "env:dev"}
cmdsB := &taskrun.Commands{UoW: uow.NewPG(poolB), Ctx: ctxmgr.NewContextManager(poolB, nil, ctxmgr.TimeDecayConfig{}), Env: "dev", StorageIdentity: "env:dev"}
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
_, _ = cmdsA.Finalize(context.Background(), rec, res)
}()
go func() {
defer wg.Done()
_, _ = cmdsB.Cancel(context.Background(), rec)
}()
wg.Wait()
var status string
if err := poolA.QueryRow(context.Background(), `SELECT status FROM task_runs WHERE run_id=$1`, rec.RunID).Scan(&status); err != nil {
t.Fatal(err)
}
if status != "completed" && status != "cancelled" {
t.Fatalf("illegal terminal %q", status)
}
var n int
_ = poolA.QueryRow(context.Background(), `SELECT COUNT(*) FROM chat_messages WHERE session_id=$1 AND role='assistant'`, sessionID).Scan(&n)
if n != 1 {
t.Fatalf("assistant turns=%d", n)
}
}
func TestPG_SSEDoneNotEmittedWhenCommitFails(t *testing.T) {
poolA, _ := openIsolatedM3(t)
userID, projectID := seedUserAndProject(t, poolA)
ctxMgr := ctxmgr.NewContextManager(poolA, nil, ctxmgr.TimeDecayConfig{})
sessionID := "sess_u8_sse"
asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), sessionID, userID, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{"project_id": projectID})
if err != nil {
t.Fatal(err)
}
rec := &taskrun.RunRecord{
RunID: "run_u8_sse", SessionID: sessionID, ProjectID: projectID, UserID: userID,
TaskKey: "intake", AssistantMsgID: asstID, Status: "pending",
}
unit := uow.NewPG(poolA)
unit.BeforeCommit = func(uow.TxScope) error { return errors.New("inject commit fail") }
cmds := &taskrun.Commands{UoW: unit, Ctx: ctxMgr, Env: "dev", StorageIdentity: "env:dev"}
mgr := taskrun.NewManager(nil, ctxMgr)
mgr.SetStore(taskrun.NewPGStore(poolA))
mgr.SetCommands(cmds)
mgr.SetExecutor(func(context.Context, string, bizdev.TaskRunRequest, *stream.Streamer) (*bizdev.TaskRunResult, error) {
return &bizdev.TaskRunResult{Success: true, MarkdownBody: "# sse\n"}, nil
}, nil, nil)
if _, err := mgr.CreateAndStart(context.Background(), rec.RunID, rec, bizdev.TaskRunRequest{TaskKey: "intake"}, bizdev.StageRunRequest{}, ""); err != nil {
t.Fatal(err)
}
deadline := time.Now().Add(8 * time.Second)
for time.Now().Before(deadline) {
got, err := mgr.GetRun(rec.RunID)
if err == nil && got.Status == "finalizing_failed" {
evs, listErr := taskrun.NewPGStore(poolA).ListEvents(context.Background(), rec.RunID, 0)
if listErr != nil {
t.Fatal(listErr)
}
for _, ev := range evs {
if ev.Event.Type == stream.EventDone {
t.Fatal("done emitted on failed commit")
}
}
msg, _ := ctxMgr.GetMessageByID(context.Background(), asstID)
if msg != nil && msg.Content != "任务执行中…" {
t.Fatalf("placeholder mutated after rollback: %q", msg.Content)
}
return
}
time.Sleep(30 * time.Millisecond)
}
t.Fatal("expected finalizing_failed")
}
func TestPG_CleanupWithoutReceiptFailsAndStagingSurvives(t *testing.T) {
poolA, _ := openIsolatedM3(t)
userID, projectID := seedUserAndProject(t, poolA)
ctxMgr := ctxmgr.NewContextManager(poolA, nil, ctxmgr.TimeDecayConfig{})
sessionID := "sess_u8_clean"
asstID, err := ctxMgr.RecordUserMessageWithID(context.Background(), sessionID, userID, "assistant", "任务执行中…", "TRIGGER_TASK", "", map[string]any{"project_id": projectID})
if err != nil {
t.Fatal(err)
}
dataRoot := t.TempDir()
cap := workspace.NewCapability(dataRoot, nil)
ref, err := cap.Issue(workspace.WorkspaceRef{ID: "ws_u8_clean", ProjectScope: projectID, RunID: "run_u8_clean"})
if err != nil {
t.Fatal(err)
}
body := []byte("# vaulted body for cleanup\n")
if err := cap.WriteArtifact(context.Background(), ref, workspace.DeliverableKey, body); err != nil {
t.Fatal(err)
}
rec := &taskrun.RunRecord{
RunID: "run_u8_clean", SessionID: sessionID, ProjectID: projectID, UserID: userID,
TaskKey: "intake", AssistantMsgID: asstID, WorkspaceID: ref.ID,
}
insertRunning(t, poolA, rec)
res := &bizdev.TaskRunResult{
Success: true, TaskKey: "intake", MarkdownBody: string(body), WorkDir: ref.ID,
DocRecord: &docgov.DocumentRecord{DocID: "doc_u8_clean", ContentSHA256: artifactvault.SHA256Hex(body), Version: 2},
}
cmds := &taskrun.Commands{UoW: uow.NewPG(poolA), Ctx: ctxMgr, Fav: &artifactvault.Ingest{Env: "dev"}, Env: "dev", StorageIdentity: "env:dev"}
if _, err := cmds.Finalize(context.Background(), rec, res); err != nil {
t.Fatal(err)
}
var manSHA, storageID string
var members []byte
if err := poolA.QueryRow(context.Background(), `SELECT sha256, storage_identity, members FROM task_run_manifests WHERE run_id=$1`, rec.RunID).
Scan(&manSHA, &storageID, &members); err != nil {
t.Fatal(err)
}
var parsed []taskrun.ManifestMember
if err := json.Unmarshal(members, &parsed); err != nil {
t.Fatal(err)
}
frozen := workspace.FrozenManifest{SHA256: manSHA, StorageIdentity: storageID, WorkspaceID: ref.ID}
for _, m := range parsed {
frozen.Members = append(frozen.Members, workspace.FrozenMember{LogicalID: m.LogicalID, Digest: m.Digest, Kind: m.Kind})
}
if err := cap.Cleanup(context.Background(), ref, frozen, nil); err == nil {
t.Fatal("cleanup without receipts must fail")
}
if _, err := os.Stat(filepath.Join(dataRoot, "workspaces", ref.ID, workspace.DeliverableKey)); err != nil {
t.Fatalf("workspace removed without receipts: %v", err)
}
var receipts []workspace.RetentionReceipt
for _, m := range parsed {
receipts = append(receipts, workspace.RetentionReceipt{
LogicalID: m.LogicalID, Digest: m.Digest, StorageIdentity: storageID, Verified: true,
})
}
if err := cap.Cleanup(context.Background(), ref, frozen, receipts); err != nil {
t.Fatal(err)
}
eventID := "taskrun:" + rec.RunID + ":completed"
reg := artifactvault.NewPGRegistry(poolA)
st, err := reg.LoadStaging(context.Background(), eventID)
if err != nil {
t.Fatal(err)
}
if artifactvault.SHA256Hex(st.Payload) != artifactvault.SHA256Hex(body) {
t.Fatalf("staging sha drifted")
}
}