test(service): add tests for task defaults and job status

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
dailz
2026-04-20 10:38:49 +08:00
parent 166ca3092c
commit e90904cedb
3 changed files with 1123 additions and 0 deletions

View File

@@ -829,6 +829,356 @@ func TestGetJob_FallbackToHistory_HistoryError(t *testing.T) {
}
}
// ---------------------------------------------------------------------------
// New scheduling field mapping tests
// ---------------------------------------------------------------------------
func TestSubmitJob_AllSchedulingFields(t *testing.T) {
jobID := int32(999)
// Prepare all 22 new scheduling field values
var (
memoryPerNode = int64(4096)
memoryPerCpu = int64(1024)
nodes = "2"
tasks = int32(4)
cpusPerTask = int32(2)
constraints = "gpu&fast"
reservation = "resv01"
account = "proj-alpha"
nice = int32(100)
mailType = "BEGIN,END,FAIL"
mailUser = "admin@example.com"
stdOut = "/tmp/job_%j.out"
stdErr = "/tmp/job_%j.err"
stdIn = "/dev/null"
reqNodes = "node01,node02"
exclNodes = "node03,node04"
beginTime = int64(1700000000)
deadline = int64(1700099999)
array = "1-10"
dependency = "afterok:123"
requeue = true
killOnNodeFail = true
)
client, cleanup := mockJobServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body slurm.JobSubmitReq
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode body: %v", err)
}
if body.Job == nil {
t.Fatal("job desc is nil")
}
j := body.Job
// --- Existing fields still work ---
if j.Script == nil || *j.Script != "#!/bin/bash\necho test" {
t.Errorf("Script mismatch: %v", j.Script)
}
if j.Partition == nil || *j.Partition != "normal" {
t.Errorf("Partition mismatch: %v", j.Partition)
}
if j.Qos == nil || *j.Qos != "high" {
t.Errorf("QOS mismatch: %v", j.Qos)
}
if j.Name == nil || *j.Name != "full-test" {
t.Errorf("Name mismatch: %v", j.Name)
}
if j.MinimumCpus == nil || *j.MinimumCpus != int32(8) {
t.Errorf("MinimumCpus mismatch: %v", j.MinimumCpus)
}
// --- 22 new scheduling fields ---
// MemoryPerNode → *Uint64NoVal
if j.MemoryPerNode == nil || j.MemoryPerNode.Number == nil || *j.MemoryPerNode.Number != memoryPerNode {
t.Errorf("MemoryPerNode mismatch: %v", j.MemoryPerNode)
}
// MemoryPerCpu → *Uint64NoVal
if j.MemoryPerCpu == nil || j.MemoryPerCpu.Number == nil || *j.MemoryPerCpu.Number != memoryPerCpu {
t.Errorf("MemoryPerCpu mismatch: %v", j.MemoryPerCpu)
}
// Nodes → *string
if j.Nodes == nil || *j.Nodes != nodes {
t.Errorf("Nodes mismatch: %v", j.Nodes)
}
// Tasks → *int32
if j.Tasks == nil || *j.Tasks != tasks {
t.Errorf("Tasks mismatch: got %v, want %d", j.Tasks, tasks)
}
// CpusPerTask → *int32
if j.CpusPerTask == nil || *j.CpusPerTask != cpusPerTask {
t.Errorf("CpusPerTask mismatch: got %v, want %d", j.CpusPerTask, cpusPerTask)
}
// Constraints → *string
if j.Constraints == nil || *j.Constraints != constraints {
t.Errorf("Constraints mismatch: %v", j.Constraints)
}
// Reservation → *string
if j.Reservation == nil || *j.Reservation != reservation {
t.Errorf("Reservation mismatch: %v", j.Reservation)
}
// Account → *string
if j.Account == nil || *j.Account != account {
t.Errorf("Account mismatch: %v", j.Account)
}
// Nice → *int32
if j.Nice == nil || *j.Nice != nice {
t.Errorf("Nice mismatch: got %v, want %d", j.Nice, nice)
}
// MailType → []string (comma-split)
if len(j.MailType) != 3 || j.MailType[0] != "BEGIN" || j.MailType[1] != "END" || j.MailType[2] != "FAIL" {
t.Errorf("MailType mismatch: %v", j.MailType)
}
// MailUser → *string
if j.MailUser == nil || *j.MailUser != mailUser {
t.Errorf("MailUser mismatch: %v", j.MailUser)
}
// StandardOutput → *string
if j.StandardOutput == nil || *j.StandardOutput != stdOut {
t.Errorf("StandardOutput mismatch: %v", j.StandardOutput)
}
// StandardError → *string
if j.StandardError == nil || *j.StandardError != stdErr {
t.Errorf("StandardError mismatch: %v", j.StandardError)
}
// StandardInput → *string
if j.StandardInput == nil || *j.StandardInput != stdIn {
t.Errorf("StandardInput mismatch: %v", j.StandardInput)
}
// RequiredNodes → CSVString ([]string)
if len(j.RequiredNodes) != 2 || j.RequiredNodes[0] != "node01" || j.RequiredNodes[1] != "node02" {
t.Errorf("RequiredNodes mismatch: %v", j.RequiredNodes)
}
// ExcludedNodes → CSVString ([]string)
if len(j.ExcludedNodes) != 2 || j.ExcludedNodes[0] != "node03" || j.ExcludedNodes[1] != "node04" {
t.Errorf("ExcludedNodes mismatch: %v", j.ExcludedNodes)
}
// BeginTime → *Uint64NoVal
if j.BeginTime == nil || j.BeginTime.Number == nil || *j.BeginTime.Number != beginTime {
t.Errorf("BeginTime mismatch: %v", j.BeginTime)
}
// Deadline → *int64 (NO wrapper)
if j.Deadline == nil || *j.Deadline != deadline {
t.Errorf("Deadline mismatch: %v", j.Deadline)
}
// Array → *string
if j.Array == nil || *j.Array != array {
t.Errorf("Array mismatch: %v", j.Array)
}
// Dependency → *string
if j.Dependency == nil || *j.Dependency != dependency {
t.Errorf("Dependency mismatch: %v", j.Dependency)
}
// Requeue → *bool
if j.Requeue == nil || *j.Requeue != requeue {
t.Errorf("Requeue mismatch: %v", j.Requeue)
}
// KillOnNodeFail → *bool
if j.KillOnNodeFail == nil || *j.KillOnNodeFail != killOnNodeFail {
t.Errorf("KillOnNodeFail mismatch: %v", j.KillOnNodeFail)
}
resp := slurm.OpenapiJobSubmitResponse{
Result: &slurm.JobSubmitResponseMsg{JobID: &jobID},
}
json.NewEncoder(w).Encode(resp)
}))
defer cleanup()
svc := NewJobService(client, zap.NewNop())
resp, err := svc.SubmitJob(context.Background(), &model.SubmitJobRequest{
Script: "#!/bin/bash\necho test",
Partition: "normal",
QOS: "high",
JobName: "full-test",
CPUs: 8,
TimeLimit: "60",
MemoryPerNode: &memoryPerNode,
MemoryPerCpu: &memoryPerCpu,
Nodes: &nodes,
Tasks: &tasks,
CpusPerTask: &cpusPerTask,
Constraints: &constraints,
Reservation: &reservation,
Account: &account,
Nice: &nice,
MailType: &mailType,
MailUser: &mailUser,
StandardOutput: &stdOut,
StandardError: &stdErr,
StandardInput: &stdIn,
RequiredNodes: &reqNodes,
ExcludedNodes: &exclNodes,
BeginTime: &beginTime,
Deadline: &deadline,
Array: &array,
Dependency: &dependency,
Requeue: &requeue,
KillOnNodeFail: &killOnNodeFail,
})
if err != nil {
t.Fatalf("SubmitJob: %v", err)
}
if resp.JobID != 999 {
t.Errorf("expected JobID 999, got %d", resp.JobID)
}
}
func TestSubmitJob_BackwardCompat(t *testing.T) {
jobID := int32(555)
client, cleanup := mockJobServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body slurm.JobSubmitReq
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode body: %v", err)
}
if body.Job == nil {
t.Fatal("job desc is nil")
}
j := body.Job
// Existing fields: Script and WorkDir should be set
if j.Script == nil || *j.Script != "echo hi" {
t.Errorf("Script mismatch: %v", j.Script)
}
if j.CurrentWorkingDirectory == nil || *j.CurrentWorkingDirectory != "/home/user" {
t.Errorf("CurrentWorkingDirectory mismatch: %v", j.CurrentWorkingDirectory)
}
// All new scheduling fields should be nil/empty
if j.MemoryPerNode != nil {
t.Errorf("MemoryPerNode should be nil, got %v", j.MemoryPerNode)
}
if j.MemoryPerCpu != nil {
t.Errorf("MemoryPerCpu should be nil, got %v", j.MemoryPerCpu)
}
if j.Nodes != nil {
t.Errorf("Nodes should be nil, got %v", j.Nodes)
}
if j.Tasks != nil {
t.Errorf("Tasks should be nil, got %v", j.Tasks)
}
if j.CpusPerTask != nil {
t.Errorf("CpusPerTask should be nil, got %v", j.CpusPerTask)
}
if j.Constraints != nil {
t.Errorf("Constraints should be nil, got %v", j.Constraints)
}
if j.Reservation != nil {
t.Errorf("Reservation should be nil, got %v", j.Reservation)
}
if j.Account != nil {
t.Errorf("Account should be nil, got %v", j.Account)
}
if j.Nice != nil {
t.Errorf("Nice should be nil, got %v", j.Nice)
}
if len(j.MailType) != 0 {
t.Errorf("MailType should be empty, got %v", j.MailType)
}
if j.MailUser != nil {
t.Errorf("MailUser should be nil, got %v", j.MailUser)
}
if j.StandardOutput != nil {
t.Errorf("StandardOutput should be nil, got %v", j.StandardOutput)
}
if j.StandardError != nil {
t.Errorf("StandardError should be nil, got %v", j.StandardError)
}
if j.StandardInput != nil {
t.Errorf("StandardInput should be nil, got %v", j.StandardInput)
}
if len(j.RequiredNodes) != 0 {
t.Errorf("RequiredNodes should be empty, got %v", j.RequiredNodes)
}
if len(j.ExcludedNodes) != 0 {
t.Errorf("ExcludedNodes should be empty, got %v", j.ExcludedNodes)
}
if j.BeginTime != nil {
t.Errorf("BeginTime should be nil, got %v", j.BeginTime)
}
if j.Deadline != nil {
t.Errorf("Deadline should be nil, got %v", j.Deadline)
}
if j.Array != nil {
t.Errorf("Array should be nil, got %v", j.Array)
}
if j.Dependency != nil {
t.Errorf("Dependency should be nil, got %v", j.Dependency)
}
if j.Requeue != nil {
t.Errorf("Requeue should be nil, got %v", j.Requeue)
}
if j.KillOnNodeFail != nil {
t.Errorf("KillOnNodeFail should be nil, got %v", j.KillOnNodeFail)
}
resp := slurm.OpenapiJobSubmitResponse{
Result: &slurm.JobSubmitResponseMsg{JobID: &jobID},
}
json.NewEncoder(w).Encode(resp)
}))
defer cleanup()
svc := NewJobService(client, zap.NewNop())
resp, err := svc.SubmitJob(context.Background(), &model.SubmitJobRequest{
Script: "echo hi",
WorkDir: "/home/user",
})
if err != nil {
t.Fatalf("SubmitJob: %v", err)
}
if resp.JobID != 555 {
t.Errorf("expected JobID 555, got %d", resp.JobID)
}
}
func TestSubmitJob_MemoryBothSet(t *testing.T) {
jobID := int32(777)
memoryPerNode := int64(4096)
memoryPerCpu := int64(1024)
client, cleanup := mockJobServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body slurm.JobSubmitReq
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode body: %v", err)
}
if body.Job == nil {
t.Fatal("job desc is nil")
}
j := body.Job
// Both memory fields should be mapped independently
if j.MemoryPerNode == nil || j.MemoryPerNode.Number == nil || *j.MemoryPerNode.Number != memoryPerNode {
t.Errorf("MemoryPerNode mismatch: %v", j.MemoryPerNode)
}
if j.MemoryPerCpu == nil || j.MemoryPerCpu.Number == nil || *j.MemoryPerCpu.Number != memoryPerCpu {
t.Errorf("MemoryPerCpu mismatch: %v", j.MemoryPerCpu)
}
resp := slurm.OpenapiJobSubmitResponse{
Result: &slurm.JobSubmitResponseMsg{JobID: &jobID},
}
json.NewEncoder(w).Encode(resp)
}))
defer cleanup()
svc := NewJobService(client, zap.NewNop())
resp, err := svc.SubmitJob(context.Background(), &model.SubmitJobRequest{
Script: "echo mem",
MemoryPerNode: &memoryPerNode,
MemoryPerCpu: &memoryPerCpu,
})
if err != nil {
t.Fatalf("SubmitJob: %v", err)
}
if resp.JobID != 777 {
t.Errorf("expected JobID 777, got %d", resp.JobID)
}
}
func TestGetJob_FallbackToHistory_EmptyHistory(t *testing.T) {
client, cleanup := mockJobServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")