Resolve conflicts: both CURRENT claim tables (slot preserved), local multi-host coordination plan + origin phased companion, origin module-boundaries, combined PG isolation with remote product tests. FEATURE-MATRIX takes origin record_id layout; A-G done bump deferred pending evidence row apply.
174 lines
5.6 KiB
Go
174 lines
5.6 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 pgResumeConfig(t *testing.T) *config.Config {
|
|
t.Helper()
|
|
cfg := configFromTestDatabaseURL(t)
|
|
cfg.DataDir = t.TempDir()
|
|
cfg.AgentBackend = "stub"
|
|
cfg.AllowInMemory = false
|
|
return cfg
|
|
}
|
|
|
|
func TestTaskRun_ResumeInterrupted_PG(t *testing.T) {
|
|
pool := openPGPoolWithMigrations(t)
|
|
if pool == nil {
|
|
return
|
|
}
|
|
defer pool.Close()
|
|
|
|
cfg := pgResumeConfig(t)
|
|
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(cfg.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)
|
|
projectID := seedProjectForRun(t, pool, "run-resume", userID)
|
|
runID := "run_resume_" + time.Now().Format("150405.000000")
|
|
_, err := pool.Exec(context.Background(), `
|
|
INSERT INTO task_runs (
|
|
run_id, session_id, project_id, user_id, task_key, status, workspace_id, error_message, created_at, updated_at
|
|
) VALUES ($1, 'sess_resume', $2, $3, 'intake', 'interrupted', $4, 'worker lease expired', now(), now())
|
|
`, runID, projectID, 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()
|
|
|
|
cfg := pgResumeConfig(t)
|
|
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(cfg.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()
|
|
|
|
cfg := pgResumeConfig(t)
|
|
handler := server.NewServer(cfg, bizdev.MustNewService(t, cfg)).Routes()
|
|
token := loginTestUser(t, handler, "zhanggong", "123456")
|
|
userID := fetchAuthUserID(t, handler, token)
|
|
workDir := filepath.Join(cfg.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"])
|
|
}
|
|
}
|