diff --git a/src/frontend/apiserver/backend/convert.go b/src/frontend/apiserver/backend/convert.go index b571e57f..5df2b2af 100644 --- a/src/frontend/apiserver/backend/convert.go +++ b/src/frontend/apiserver/backend/convert.go @@ -201,9 +201,19 @@ func cJobToModel(job *C.xce_job_t) *models.JobInfo { // baselines encode main_reason=0 detail-only PENDs). if job.pend_reasons != nil { pr := job.pend_reasons + // main_reason 0 is detail-only (the allocator's ordinary shortfall + // shape): leave the NAME empty so rawJobToModel's promotion fills it + // from the first detail row. Pre-filling pendReasonName(0) here + // ("unknown(0)") made the promotion unreachable on the live C path — + // the advisor P0 verification caught jobs whose res_reasons named the + // real cause while main_reason_name read unknown(0). + mainName := "" + if pr.main_reason != 0 { + mainName = pendReasonName(int(pr.main_reason)) + } r.pendReason = &rawPendReason{ MainReason: int(pr.main_reason), - MainReasonName: pendReasonName(int(pr.main_reason)), + MainReasonName: mainName, } for i := 0; i < int(pr.nhost_reasons); i++ { hr := C.job_host_reason(pr, C.int(i)) diff --git a/src/frontend/apiserver/backend/convert_test.go b/src/frontend/apiserver/backend/convert_test.go index 11ea252f..af851471 100644 --- a/src/frontend/apiserver/backend/convert_test.go +++ b/src/frontend/apiserver/backend/convert_test.go @@ -1656,3 +1656,46 @@ func TestFormatJobID_ArrayChild(t *testing.T) { t.Errorf("FormatJobID(array) = %q, want %q", got, want) } } + +// The C path (cJobToModel) must NOT pre-fill main_reason_name for a +// detail-only record: pendReasonName(0) = "unknown(0)" made the pure-layer +// promotion unreachable on the live path (advisor P0 verification finding — +// jobs whose res_reasons named the real cause while main_reason_name read +// unknown(0)). This pins the shape cJobToModel now hands to rawJobToModel: +// main_reason 0 with an EMPTY name (res detail rows as the C record carried +// them) must promote no_resource, and a non-zero main reason keeps its name. +func TestPendReasonShape_CPathDetailOnlyPromotes(t *testing.T) { + // what cJobToModel builds for main_reason 0 since the fix: name empty + r := rawJobData{ + jobID: 11, + state: 1, + pendReason: &rawPendReason{ + MainReason: 0, + MainReasonName: "", // cJobToModel leaves it for the promotion + ResReasons: []rawResReason{{Resource: "limit=limit1", Host: "", Reason: 2}}, + }, + } + info := rawJobToModel(r) + var decoded struct { + MainReason int `json:"main_reason"` + MainReasonName string `json:"main_reason_name"` + } + if err := json.Unmarshal([]byte(info.PendingReason.String), &decoded); err != nil { + t.Fatalf("PendingReason invalid JSON: %v", err) + } + if decoded.MainReasonName != "no_resource" { + t.Errorf("detail-only via C shape: main_reason_name = %q, want promoted no_resource", decoded.MainReasonName) + } + + // non-zero main reason keeps the mapped name (promotion must not touch it) + r2 := r + r2.pendReason = &rawPendReason{MainReason: 6, MainReasonName: "queue_closed"} + info2 := rawJobToModel(r2) + var d2 struct { + MainReasonName string `json:"main_reason_name"` + } + _ = json.Unmarshal([]byte(info2.PendingReason.String), &d2) + if d2.MainReasonName != "queue_closed" { + t.Errorf("explicit main reason overwritten: got %q, want queue_closed", d2.MainReasonName) + } +}