Files
Agent-System/internal/server/task_run_resume_pg_test.go
T
weichenandCursor d7a144de26 feat(taskrun): resume interrupted runs and harden P1 security gaps
Add confirm-gated resume API with resumable flag, Phase 5 finalize fault
and rolling-restart replay tests, Mermaid strict mode, request body limits,
and optional Runner Bearer auth between app and pi-agent.

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

182 lines
6.2 KiB
Go

package server_test
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"time"
"chat-agent-system/internal/bizdev"
"chat-agent-system/internal/config"
"chat-agent-system/internal/server"
"chat-agent-system/internal/stream"
"chat-agent-system/internal/taskrun"
)
func TestTaskRun_ResumeInterrupted_PG(t *testing.T) {
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
dataDir := t.TempDir()
cfg := &config.Config{
Env: "dev", DataDir: dataDir, AgentBackend: "stub", AllowInMemory: false,
ContextDbHost: "127.0.0.1", ContextDbPort: 5433, ContextDbUser: "app",
ContextDbPassword: "dev_secret_db_pass", ContextDbName: "agent_system_dev",
AdminUsername: "admin", AdminPassword: "admin-secret-password-123",
}
svc := bizdev.MustNewService(t, cfg)
srv := server.NewServer(cfg, svc)
handler := srv.Routes()
token := loginTestUser(t, handler, "zhanggong", "123456")
userID := fetchAuthUserID(t, handler, token)
workDir := filepath.Join(dataDir, "workspaces", "bizdev", "resume_test")
if err := os.MkdirAll(workDir, 0o755); err != nil {
t.Fatal(err)
}
_ = os.WriteFile(filepath.Join(workDir, "output.md"), []byte("# resumed\n"), 0o644)
store := taskrun.NewPGStore(pool)
runID := "run_resume_" + time.Now().Format("150405.000000")
_, err := pool.Exec(context.Background(), `
INSERT INTO task_runs (
run_id, session_id, user_id, task_key, status, workspace_id, error_message, created_at, updated_at
) VALUES ($1, 'sess_resume', $2, 'intake', 'interrupted', $3, 'worker lease expired', now(), now())
`, runID, userID, workDir)
if err != nil {
t.Fatalf("seed interrupted run: %v", err)
}
t.Cleanup(func() {
_, _ = pool.Exec(context.Background(), `DELETE FROM task_runs WHERE run_id = $1`, runID)
})
srv.TaskRuns().SetExecutor(
func(_ context.Context, _ string, _ bizdev.TaskRunRequest, _ *stream.Streamer) (*bizdev.TaskRunResult, error) {
return &bizdev.TaskRunResult{
Success: true, WorkDir: workDir, TaskKey: "intake", ArtifactTitle: "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)
})
srv.TaskRuns().SetStore(store)
body, _ := json.Marshal(map[string]bool{"confirm": true})
req := httptest.NewRequest(http.MethodPost, "/api/v1/tasks/runs/"+runID+"/resume", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
withBearerAuth(req, token)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusAccepted {
t.Fatalf("resume expected 202, got %d %s", rec.Code, rec.Body.String())
}
deadline := time.Now().Add(8 * time.Second)
for time.Now().Before(deadline) {
got, getErr := store.GetRun(context.Background(), runID)
if getErr == nil && got.Status == "completed" {
if got.Attempt < 2 {
t.Fatalf("expected attempt>=2 after resume, got %d", got.Attempt)
}
return
}
time.Sleep(50 * time.Millisecond)
}
t.Fatal("resume did not complete")
}
func TestTaskRun_ResumeRequiresConfirm_PG(t *testing.T) {
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
dataDir := t.TempDir()
cfg := &config.Config{
Env: "dev", DataDir: dataDir, AgentBackend: "stub", AllowInMemory: false,
ContextDbHost: "127.0.0.1", ContextDbPort: 5433, ContextDbUser: "app",
ContextDbPassword: "dev_secret_db_pass", ContextDbName: "agent_system_dev",
AdminUsername: "admin", AdminPassword: "admin-secret-password-123",
}
srv := server.NewServer(cfg, bizdev.MustNewService(t, cfg))
handler := srv.Routes()
token := loginTestUser(t, handler, "zhanggong", "123456")
userID := fetchAuthUserID(t, handler, token)
workDir := filepath.Join(dataDir, "ws")
_ = os.MkdirAll(workDir, 0o755)
runID := "run_no_confirm_" + time.Now().Format("150405.000000")
_, _ = pool.Exec(context.Background(), `
INSERT INTO task_runs (run_id, session_id, user_id, task_key, status, workspace_id, created_at, updated_at)
VALUES ($1, 's', $2, 'intake', 'interrupted', $3, now(), now())
`, runID, userID, workDir)
t.Cleanup(func() {
_, _ = pool.Exec(context.Background(), `DELETE FROM task_runs WHERE run_id = $1`, runID)
})
_ = srv
req := httptest.NewRequest(http.MethodPost, "/api/v1/tasks/runs/"+runID+"/resume", bytes.NewReader([]byte(`{"confirm":false}`)))
req.Header.Set("Content-Type", "application/json")
withBearerAuth(req, token)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("expected 400 without confirm, got %d", rec.Code)
}
}
func TestTaskRun_GetRunShowsResumable_PG(t *testing.T) {
pool := openPGPoolWithMigrations(t)
if pool == nil {
return
}
defer pool.Close()
dataDir := t.TempDir()
cfg := &config.Config{
Env: "dev", DataDir: dataDir, AgentBackend: "stub", AllowInMemory: false,
ContextDbHost: "127.0.0.1", ContextDbPort: 5433, ContextDbUser: "app",
ContextDbPassword: "dev_secret_db_pass", ContextDbName: "agent_system_dev",
AdminUsername: "admin", AdminPassword: "admin-secret-password-123",
}
handler := server.NewServer(cfg, bizdev.MustNewService(t, cfg)).Routes()
token := loginTestUser(t, handler, "zhanggong", "123456")
userID := fetchAuthUserID(t, handler, token)
workDir := filepath.Join(dataDir, "workspaces", "bizdev", "resumable_flag")
_ = os.MkdirAll(workDir, 0o755)
runID := "run_flag_" + time.Now().Format("150405.000000")
_, _ = pool.Exec(context.Background(), `
INSERT INTO task_runs (run_id, session_id, user_id, task_key, status, workspace_id, created_at, updated_at)
VALUES ($1, 's', $2, 'intake', 'interrupted', $3, now(), now())
`, runID, userID, workDir)
t.Cleanup(func() {
_, _ = pool.Exec(context.Background(), `DELETE FROM task_runs WHERE run_id = $1`, runID)
})
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/api/v1/tasks/runs/%s", runID), nil)
withBearerAuth(req, token)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("GET run: %d", rec.Code)
}
var body map[string]any
_ = json.Unmarshal(rec.Body.Bytes(), &body)
if body["resumable"] != true {
t.Fatalf("expected resumable=true, got %v", body["resumable"])
}
}