Files
hpc/cmd/server/integration_job_test.go
dailz b90942de77 fix(task): prevent duplicate Slurm job submission on backend restart
RecoverStuckTasks now skips tasks that already have a slurm_job_id,
and ProcessTask adds a guard before the submitting step to prevent
re-submission even if a task is incorrectly re-enqueued.

Also deprecates POST /api/v1/jobs/submit endpoint (replaced by POST /tasks)
and comments out related handlers and tests.
2026-04-21 10:57:38 +08:00

225 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"bytes"
"encoding/json"
"net/http"
"testing"
"gcy_hpc_server/internal/testutil/testenv"
)
// jobItemData mirrors the JobResponse DTO for a single job.
type jobItemData struct {
JobID int32 `json:"job_id"`
Name string `json:"name"`
State []string `json:"job_state"`
Partition string `json:"partition"`
}
// jobListData mirrors the paginated JobListResponse DTO.
type jobListData struct {
Jobs []jobItemData `json:"jobs"`
Total int `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// jobCancelData mirrors the cancel response message.
type jobCancelData struct {
Message string `json:"message"`
}
// jobDecodeAll decodes the response and returns status, success, and raw data.
func jobDecodeAll(env *testenv.TestEnv, resp *http.Response) (statusCode int, success bool, data json.RawMessage, err error) {
statusCode = resp.StatusCode
success, data, err = env.DecodeResponse(resp)
return
}
// jobSubmitBody builds a JSON body for job submit requests.
func jobSubmitBody(script string) *bytes.Reader {
body, _ := json.Marshal(map[string]string{"script": script})
return bytes.NewReader(body)
}
// jobSubmitViaAPI submits a job and returns the job ID. Fatals on failure.
func jobSubmitViaAPI(t *testing.T, env *testenv.TestEnv, script string) int32 {
t.Helper()
resp := env.DoRequest(http.MethodPost, "/api/v1/jobs/submit", jobSubmitBody(script))
status, success, data, err := jobDecodeAll(env, resp)
if err != nil {
t.Fatalf("submit job decode: %v", err)
}
if status != http.StatusCreated {
t.Fatalf("expected status 201, got %d", status)
}
if !success {
t.Fatal("expected success=true on submit")
}
var job jobItemData
if err := json.Unmarshal(data, &job); err != nil {
t.Fatalf("unmarshal submitted job: %v", err)
}
return job.JobID
}
// [已弃用] 以下测试依赖 POST /api/v1/jobs/submit该接口已被 POST /tasks 取代。
/*
// TestIntegration_Jobs_Submit verifies POST /api/v1/jobs/submit creates a new job.
func TestIntegration_Jobs_Submit(t *testing.T) {
env := testenv.NewTestEnv(t)
script := "#!/bin/bash\necho hello"
resp := env.DoRequest(http.MethodPost, "/api/v1/jobs/submit", jobSubmitBody(script))
status, success, data, err := jobDecodeAll(env, resp)
if err != nil {
t.Fatalf("decode response: %v", err)
}
if status != http.StatusCreated {
t.Fatalf("expected status 201, got %d", status)
}
if !success {
t.Fatal("expected success=true")
}
var job jobItemData
if err := json.Unmarshal(data, &job); err != nil {
t.Fatalf("unmarshal job: %v", err)
}
if job.JobID <= 0 {
t.Fatalf("expected positive job_id, got %d", job.JobID)
}
}
// TestIntegration_Jobs_List verifies GET /api/v1/jobs returns a paginated job list.
func TestIntegration_Jobs_List(t *testing.T) {
env := testenv.NewTestEnv(t)
// Submit a job so the list is not empty.
jobSubmitViaAPI(t, env, "#!/bin/bash\necho list-test")
resp := env.DoRequest(http.MethodGet, "/api/v1/jobs", nil)
status, success, data, err := jobDecodeAll(env, resp)
if err != nil {
t.Fatalf("decode response: %v", err)
}
if status != http.StatusOK {
t.Fatalf("expected status 200, got %d", status)
}
if !success {
t.Fatal("expected success=true")
}
var list jobListData
if err := json.Unmarshal(data, &list); err != nil {
t.Fatalf("unmarshal job list: %v", err)
}
if list.Total < 1 {
t.Fatalf("expected at least 1 job, got total=%d", list.Total)
}
if list.Page != 1 {
t.Fatalf("expected page=1, got %d", list.Page)
}
}
// TestIntegration_Jobs_Get verifies GET /api/v1/jobs/:id returns a single job.
func TestIntegration_Jobs_Get(t *testing.T) {
env := testenv.NewTestEnv(t)
jobID := jobSubmitViaAPI(t, env, "#!/bin/bash\necho get-test")
path := fmt.Sprintf("/api/v1/jobs/%d", jobID)
resp := env.DoRequest(http.MethodGet, path, nil)
status, success, data, err := jobDecodeAll(env, resp)
if err != nil {
t.Fatalf("decode response: %v", err)
}
if status != http.StatusOK {
t.Fatalf("expected status 200, got %d", status)
}
if !success {
t.Fatal("expected success=true")
}
var job jobItemData
if err := json.Unmarshal(data, &job); err != nil {
t.Fatalf("unmarshal job: %v", err)
}
if job.JobID != jobID {
t.Fatalf("expected job_id=%d, got %d", jobID, job.JobID)
}
}
// TestIntegration_Jobs_Cancel verifies DELETE /api/v1/jobs/:id cancels a job.
func TestIntegration_Jobs_Cancel(t *testing.T) {
env := testenv.NewTestEnv(t)
jobID := jobSubmitViaAPI(t, env, "#!/bin/bash\necho cancel-test")
path := fmt.Sprintf("/api/v1/jobs/%d", jobID)
resp := env.DoRequest(http.MethodDelete, path, nil)
status, success, data, err := jobDecodeAll(env, resp)
if err != nil {
t.Fatalf("decode response: %v", err)
}
if status != http.StatusOK {
t.Fatalf("expected status 200, got %d", status)
}
if !success {
t.Fatal("expected success=true")
}
var msg jobCancelData
if err := json.Unmarshal(data, &msg); err != nil {
t.Fatalf("unmarshal cancel response: %v", err)
}
if msg.Message == "" {
t.Fatal("expected non-empty cancel message")
}
}
// TestIntegration_Jobs_History verifies GET /api/v1/jobs/history returns historical jobs.
func TestIntegration_Jobs_History(t *testing.T) {
env := testenv.NewTestEnv(t)
// Submit and cancel a job so it moves from active to history queue.
jobID := jobSubmitViaAPI(t, env, "#!/bin/bash\necho history-test")
path := fmt.Sprintf("/api/v1/jobs/%d", jobID)
env.DoRequest(http.MethodDelete, path, nil)
resp := env.DoRequest(http.MethodGet, "/api/v1/jobs/history", nil)
status, success, data, err := jobDecodeAll(env, resp)
if err != nil {
t.Fatalf("decode response: %v", err)
}
if status != http.StatusOK {
t.Fatalf("expected status 200, got %d", status)
}
if !success {
t.Fatal("expected success=true")
}
var list jobListData
if err := json.Unmarshal(data, &list); err != nil {
t.Fatalf("unmarshal history: %v", err)
}
if list.Total < 1 {
t.Fatalf("expected at least 1 history job, got total=%d", list.Total)
}
// Verify the cancelled job appears in history.
found := false
for _, j := range list.Jobs {
if j.JobID == jobID {
found = true
break
}
}
if !found {
t.Fatalf("cancelled job %d not found in history", jobID)
}
}
*/