fix(schedulerclient): parse apiserver OrderMap columnar rows (bug-1491)
The apiserver returns list rows in OrderMap columnar form —
{"Fields":["col",...],"Data":{"col":val,...}} — but the read clients
unmarshalled each row as a flat object, so every field came back empty
(counts were right, values were all zero/"").
Fix: decode rows as apiRow{Fields, Data map[string]json.RawMessage} and
pull named fields out of Data via rowStr/rowInt/rowInt64/rowFloat helpers.
ListNodes, ListQueues, and getJobs (ListJobs/GetJob) all use apiRow now.
TDD: 3 new ordermap_test.go tests built from live-captured apiserver
response shapes (red: fields empty → green: parsed). Live-verified against
t1: node name=t1 cores=8, 4 queues active, 3 jobs with real fields.
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4a4ae16725
commit
226259a52b
4 changed files with 294 additions and 56 deletions
|
|
@ -14544,5 +14544,25 @@
|
||||||
],
|
],
|
||||||
"occurrences": 2,
|
"occurrences": 2,
|
||||||
"last_seen": "2026-08-01"
|
"last_seen": "2026-08-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "bug-1491",
|
||||||
|
"timestamp": "2026-08-02T12:00:00+08:00",
|
||||||
|
"error_message": "scheduler_list_nodes/queues/jobs MCP read tools return correct entity count but all fields empty (name='', status='unknown', 0 resources); raw_status empty",
|
||||||
|
"file": "src/frontend/schedulerclient/httpclient.go",
|
||||||
|
"root_cause": "apiserver /xce/v1/hosts|queues|jobs return rows in utils.OrderMap columnar format {\"Fields\":[...],\"Data\":{...}}, but schedulerclient's apiHost/apiQueue/apiJob structs unmarshal flat JSON objects, so every field is zero-valued. Entity count still correct because len(rows.Rows) works.",
|
||||||
|
"fix": "schedulerclient now decodes apiserver OrderMap columnar rows: added apiRow{Fields,Data map[string]json.RawMessage} + rowStr/rowInt/rowInt64/rowFloat helpers in httpclient.go; ListNodes/ListQueues/getJobs pull named fields from row.Data. TDD: 3 new ordermap_test.go tests with real captured response shapes (red\u2192green). Live-verified against t1: nodes name=t1 cores=8, 4 queues active, 3 jobs real fields.",
|
||||||
|
"tags": [
|
||||||
|
"schedulerclient",
|
||||||
|
"ordermap",
|
||||||
|
"read-parse",
|
||||||
|
"phase1-verify",
|
||||||
|
"fixed"
|
||||||
|
],
|
||||||
|
"related_bugs": [
|
||||||
|
"bug-1490"
|
||||||
|
],
|
||||||
|
"occurrences": 4,
|
||||||
|
"last_seen": "2026-08-02"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -7278,3 +7278,15 @@
|
||||||
| 09:56 | Pushed chatbot branch to myxce (2e299f4..9fe14c1, 3 commits) | git push | done | ~0.2k tok |
|
| 09:56 | Pushed chatbot branch to myxce (2e299f4..9fe14c1, 3 commits) | git push | done | ~0.2k tok |
|
||||||
| 09:58 | Wrote src/docs/chatbot-verification.md (cluster deploy + Phase 0-1 verify manual), added to design doc index, committed 56da673 + pushed | docs | done | ~1k tok |
|
| 09:58 | Wrote src/docs/chatbot-verification.md (cluster deploy + Phase 0-1 verify manual), added to design doc index, committed 56da673 + pushed | docs | done | ~1k tok |
|
||||||
| 04:26 | Added §5 "接入 Claude Code" to chatbot-verification.md (verified via claude-code-guide against installed CLI v2.1.220: claude mcp add / .mcp.json schema / scopes / approval gate) | src/docs/chatbot-verification.md | done | ~1k tok |
|
| 04:26 | Added §5 "接入 Claude Code" to chatbot-verification.md (verified via claude-code-guide against installed CLI v2.1.220: claude mcp add / .mcp.json schema / scopes / approval gate) | src/docs/chatbot-verification.md | done | ~1k tok |
|
||||||
|
|
||||||
|
## Session: 2026-08-02 04:45
|
||||||
|
|
||||||
|
| Time | Action | File(s) | Outcome | ~Tokens |
|
||||||
|
|------|--------|---------|---------|--------|
|
||||||
|
|
||||||
|
## Session: 2026-08-02 04:50
|
||||||
|
|
||||||
|
| Time | Action | File(s) | Outcome | ~Tokens |
|
||||||
|
|------|--------|---------|---------|--------|
|
||||||
|
| 12:05 | 查询集群机器: MCP 只读工具(list_nodes/queues/jobs)返回空字段 → 根因: apiserver 返回 OrderMap 列式格式 {Fields,Data}, schedulerclient 用扁平 struct 解析 | src/frontend/schedulerclient/httpclient.go | 记 bug-1491, 未修 | ~1k tok |
|
||||||
|
| 05:00 | Fixed bug-1491: schedulerclient read clients now parse apiserver OrderMap columnar rows (apiRow + rowStr/rowInt/rowInt64/rowFloat). TDD: 3 ordermap_test.go red→green; live-verified on t1 (nodes/queues/jobs fields all correct) | src/frontend/schedulerclient/httpclient.go, ordermap_test.go | green | ~1k tok |
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,8 @@ func (hc *HTTPClient) ListQueues(ctx context.Context) ([]*URMQueue, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows struct {
|
var rows struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Rows []apiQueue `json:"rows"`
|
Rows []apiRow `json:"rows"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(env.Data, &rows); err != nil {
|
if err := json.Unmarshal(env.Data, &rows); err != nil {
|
||||||
return nil, fmt.Errorf("parse queues: %w", err)
|
return nil, fmt.Errorf("parse queues: %w", err)
|
||||||
|
|
@ -134,19 +134,20 @@ func (hc *HTTPClient) ListQueues(ctx context.Context) ([]*URMQueue, error) {
|
||||||
queues := make([]*URMQueue, 0, len(rows.Rows))
|
queues := make([]*URMQueue, 0, len(rows.Rows))
|
||||||
for i := range rows.Rows {
|
for i := range rows.Rows {
|
||||||
q := rows.Rows[i]
|
q := rows.Rows[i]
|
||||||
|
rawStatus := q.rowStr("queue_status")
|
||||||
queues = append(queues, &URMQueue{
|
queues = append(queues, &URMQueue{
|
||||||
Name: q.QueueName,
|
Name: q.rowStr("queue_name"),
|
||||||
Status: QueueStatusFromString(q.QueueStatus),
|
Status: QueueStatusFromString(rawStatus),
|
||||||
RunningJobs: q.NumRun,
|
RunningJobs: q.rowInt("num_run"),
|
||||||
PendingJobs: q.NumPend,
|
PendingJobs: q.rowInt("num_pend"),
|
||||||
SuspendedJobs: q.NumSSusp + q.NumUSusp,
|
SuspendedJobs: q.rowInt("num_ssusp") + q.rowInt("num_ususp"),
|
||||||
NodeCount: 0, // apiserver does not expose queue→node mapping directly
|
NodeCount: 0, // apiserver does not expose queue→node mapping directly
|
||||||
Extensions: map[string]any{
|
Extensions: map[string]any{
|
||||||
"raw_status": q.QueueStatus,
|
"raw_status": rawStatus,
|
||||||
"priority": q.Priority,
|
"priority": q.rowInt("priority"),
|
||||||
"num_ssusp": q.NumSSusp,
|
"num_ssusp": q.rowInt("num_ssusp"),
|
||||||
"num_ususp": q.NumUSusp,
|
"num_ususp": q.rowInt("num_ususp"),
|
||||||
"num_done": q.NumDone,
|
"num_done": q.rowInt("num_done"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -168,8 +169,8 @@ func (hc *HTTPClient) ListNodes(ctx context.Context) ([]*URMNode, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows struct {
|
var rows struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Rows []apiHost `json:"rows"`
|
Rows []apiRow `json:"rows"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(env.Data, &rows); err != nil {
|
if err := json.Unmarshal(env.Data, &rows); err != nil {
|
||||||
return nil, fmt.Errorf("parse nodes: %w", err)
|
return nil, fmt.Errorf("parse nodes: %w", err)
|
||||||
|
|
@ -178,26 +179,28 @@ func (hc *HTTPClient) ListNodes(ctx context.Context) ([]*URMNode, error) {
|
||||||
nodes := make([]*URMNode, 0, len(rows.Rows))
|
nodes := make([]*URMNode, 0, len(rows.Rows))
|
||||||
for i := range rows.Rows {
|
for i := range rows.Rows {
|
||||||
h := rows.Rows[i]
|
h := rows.Rows[i]
|
||||||
|
rawStatus := h.rowStr("host_status")
|
||||||
|
numRun := h.rowInt("num_run")
|
||||||
nodes = append(nodes, &URMNode{
|
nodes = append(nodes, &URMNode{
|
||||||
Name: h.HostName,
|
Name: h.rowStr("host_name"),
|
||||||
Status: NodeStatusFromString(h.HostStatus),
|
Status: NodeStatusFromString(rawStatus),
|
||||||
CoresTotal: h.NumCPUs,
|
CoresTotal: h.rowInt("num_cpus"),
|
||||||
CoresUsed: h.NumRun, // running jobs ≈ cores used
|
CoresUsed: numRun, // running jobs ≈ cores used
|
||||||
MemoryBytesTotal: int64(h.MaxMem) * mbToBytes,
|
MemoryBytesTotal: int64(h.rowInt("max_mem")) * mbToBytes,
|
||||||
MemoryBytesUsed: 0, // apiserver does not expose live used-memory for hosts
|
MemoryBytesUsed: 0, // apiserver does not expose live used-memory for hosts
|
||||||
GPUCount: 0, // TODO: parse from extensions/topology when available
|
GPUCount: 0, // TODO: parse from extensions/topology when available
|
||||||
Features: nil,
|
Features: nil,
|
||||||
Extensions: map[string]any{
|
Extensions: map[string]any{
|
||||||
"raw_status": h.HostStatus,
|
"raw_status": rawStatus,
|
||||||
"host_type": h.HostType,
|
"host_type": h.rowStr("host_type"),
|
||||||
"host_model": h.HostModel,
|
"host_model": h.rowStr("host_model"),
|
||||||
"cpu_factor": h.CPUFactor,
|
"cpu_factor": h.rowFloat("cpu_factor"),
|
||||||
"max_slot": h.MaxSlot,
|
"max_slot": h.rowInt("max_slot"),
|
||||||
"max_swap_mb": h.MaxSwap,
|
"max_swap_mb": h.rowInt("max_swap"),
|
||||||
"max_tmp_mb": h.MaxTmp,
|
"max_tmp_mb": h.rowInt("max_tmp"),
|
||||||
"num_run": h.NumRun,
|
"num_run": numRun,
|
||||||
"num_ssusp": h.NumSSusp,
|
"num_ssusp": h.rowInt("num_ssusp"),
|
||||||
"num_ususp": h.NumUSusp,
|
"num_ususp": h.rowInt("num_ususp"),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -285,9 +288,9 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter)
|
||||||
}
|
}
|
||||||
|
|
||||||
var rows struct {
|
var rows struct {
|
||||||
Count int `json:"count"`
|
Count int `json:"count"`
|
||||||
Rows []apiJob `json:"rows"`
|
Rows []apiRow `json:"rows"`
|
||||||
PageToken string `json:"page_token"`
|
PageToken string `json:"page_token"`
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(env.Data, &rows); err != nil {
|
if err := json.Unmarshal(env.Data, &rows); err != nil {
|
||||||
return nil, nil, fmt.Errorf("parse jobs: %w", err)
|
return nil, nil, fmt.Errorf("parse jobs: %w", err)
|
||||||
|
|
@ -296,7 +299,8 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter)
|
||||||
jobs := make([]*URMJob, 0, len(rows.Rows))
|
jobs := make([]*URMJob, 0, len(rows.Rows))
|
||||||
for i := range rows.Rows {
|
for i := range rows.Rows {
|
||||||
j := rows.Rows[i]
|
j := rows.Rows[i]
|
||||||
urmStatus := JobStatusFromString(j.JobStatus)
|
rawStatus := j.rowStr("job_status")
|
||||||
|
urmStatus := JobStatusFromString(rawStatus)
|
||||||
|
|
||||||
// Apply URM status filter in post-processing
|
// Apply URM status filter in post-processing
|
||||||
if len(filter.Statuses) > 0 {
|
if len(filter.Statuses) > 0 {
|
||||||
|
|
@ -314,20 +318,20 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter)
|
||||||
|
|
||||||
// Parse exit code
|
// Parse exit code
|
||||||
var exitCode *int
|
var exitCode *int
|
||||||
if j.ExitStatus != "" {
|
if es := j.rowStr("exit_status"); es != "" {
|
||||||
if code, err := strconv.Atoi(j.ExitStatus); err == nil {
|
if code, err := strconv.Atoi(es); err == nil {
|
||||||
exitCode = &code
|
exitCode = &code
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse timestamps
|
// Parse timestamps
|
||||||
var submitTime time.Time
|
var submitTime time.Time
|
||||||
if j.SubmitTime > 0 {
|
if st := j.rowInt64("submit_time"); st > 0 {
|
||||||
submitTime = time.Unix(j.SubmitTime, 0)
|
submitTime = time.Unix(st, 0)
|
||||||
}
|
}
|
||||||
var startTime *time.Time
|
var startTime *time.Time
|
||||||
if j.StartTime > 0 {
|
if st := j.rowInt64("start_time"); st > 0 {
|
||||||
t := time.Unix(j.StartTime, 0)
|
t := time.Unix(st, 0)
|
||||||
startTime = &t
|
startTime = &t
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -337,33 +341,34 @@ func (hc *HTTPClient) getJobs(req *resty.Request, url string, filter JobFilter)
|
||||||
walltime := 0 // TODO: parse run_limit string when available
|
walltime := 0 // TODO: parse run_limit string when available
|
||||||
|
|
||||||
// Memory: MaxMem from apiserver is in MB (standard HPC convention).
|
// Memory: MaxMem from apiserver is in MB (standard HPC convention).
|
||||||
memBytes := int64(math.Round(j.MaxMem * mbToBytes))
|
memBytes := int64(math.Round(j.rowFloat("max_mem") * mbToBytes))
|
||||||
|
|
||||||
|
numProcessors := j.rowInt("num_processors")
|
||||||
urmJob := &URMJob{
|
urmJob := &URMJob{
|
||||||
ID: strconv.FormatInt(j.JobID, 10),
|
ID: strconv.FormatInt(j.rowInt64("job_id"), 10),
|
||||||
Name: j.JobName,
|
Name: j.rowStr("job_name"),
|
||||||
Status: urmStatus,
|
Status: urmStatus,
|
||||||
RawStatus: j.JobStatus,
|
RawStatus: rawStatus,
|
||||||
Queue: j.QueueName,
|
Queue: j.rowStr("queue_name"),
|
||||||
Nodes: j.NumHosts,
|
Nodes: j.rowInt("num_hosts"),
|
||||||
Cores: j.NumProcessors,
|
Cores: numProcessors,
|
||||||
GPUs: 0, // TODO: parse from resreq/extensions
|
GPUs: 0, // TODO: parse from resreq/extensions
|
||||||
MemoryBytes: memBytes,
|
MemoryBytes: memBytes,
|
||||||
WalltimeSeconds: walltime,
|
WalltimeSeconds: walltime,
|
||||||
SubmissionTime: submitTime,
|
SubmissionTime: submitTime,
|
||||||
StartTime: startTime,
|
StartTime: startTime,
|
||||||
ExitCode: exitCode,
|
ExitCode: exitCode,
|
||||||
Owner: j.SubmitUser,
|
Owner: j.rowStr("submit_user"),
|
||||||
Extensions: map[string]any{
|
Extensions: map[string]any{
|
||||||
"command": j.Command,
|
"command": j.rowStr("command"),
|
||||||
"project_name": j.ProjectName,
|
"project_name": j.rowStr("project_name"),
|
||||||
"pending_reason": j.PendingReason,
|
"pending_reason": j.rowStr("pending_reason"),
|
||||||
"resreq": j.Resreq,
|
"resreq": j.rowStr("resreq"),
|
||||||
"exit_status_raw": j.ExitStatus,
|
"exit_status_raw": j.rowStr("exit_status"),
|
||||||
"num_processors": j.NumProcessors,
|
"num_processors": numProcessors,
|
||||||
"submit_time": j.SubmitTime,
|
"submit_time": j.rowInt64("submit_time"),
|
||||||
"start_time": j.StartTime,
|
"start_time": j.rowInt64("start_time"),
|
||||||
"end_time": j.EndTime,
|
"end_time": j.rowInt64("end_time"),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -388,6 +393,67 @@ type apiEnvelope struct {
|
||||||
Data json.RawMessage `json:"data"`
|
Data json.RawMessage `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// apiRow is one row of an apiserver OrderMap response. The apiserver returns
|
||||||
|
// list rows in columnar form: {"Fields":["col",...],"Data":{"col":val,...}}
|
||||||
|
// (bug-1491: a flat-object unmarshal left every field empty). Decode rows as
|
||||||
|
// apiRow and pull named fields out of Data with the helpers below.
|
||||||
|
type apiRow struct {
|
||||||
|
Fields []string `json:"fields"`
|
||||||
|
Data map[string]json.RawMessage `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// rowStr extracts a string field. Missing, null, or non-string → "".
|
||||||
|
func (r apiRow) rowStr(key string) string {
|
||||||
|
raw, ok := r.Data[key]
|
||||||
|
if !ok || string(raw) == "null" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
var s string
|
||||||
|
if err := json.Unmarshal(raw, &s); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// rowInt extracts an integer field. Missing, null, or non-number → 0.
|
||||||
|
func (r apiRow) rowInt(key string) int {
|
||||||
|
raw, ok := r.Data[key]
|
||||||
|
if !ok || string(raw) == "null" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var f float64
|
||||||
|
if err := json.Unmarshal(raw, &f); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rowInt64 extracts a 64-bit integer field. Missing, null, or non-number → 0.
|
||||||
|
func (r apiRow) rowInt64(key string) int64 {
|
||||||
|
raw, ok := r.Data[key]
|
||||||
|
if !ok || string(raw) == "null" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var f float64
|
||||||
|
if err := json.Unmarshal(raw, &f); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return int64(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
// rowFloat extracts a float field. Missing, null, or non-number → 0.
|
||||||
|
func (r apiRow) rowFloat(key string) float64 {
|
||||||
|
raw, ok := r.Data[key]
|
||||||
|
if !ok || string(raw) == "null" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var f float64
|
||||||
|
if err := json.Unmarshal(raw, &f); err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
// apiJob mirrors the fields of apiserver's QueryJob that we need for URM conversion.
|
// apiJob mirrors the fields of apiserver's QueryJob that we need for URM conversion.
|
||||||
type apiJob struct {
|
type apiJob struct {
|
||||||
JobID int64 `json:"job_id"`
|
JobID int64 `json:"job_id"`
|
||||||
|
|
|
||||||
140
src/frontend/schedulerclient/ordermap_test.go
Normal file
140
src/frontend/schedulerclient/ordermap_test.go
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
package schedulerclient
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestOrderMap_RowsUseColumnarShape is the regression guard for bug-1491:
|
||||||
|
// the apiserver's /xce/v1/hosts|queues|jobs responses are OrderMap columnar
|
||||||
|
// rows — {"Fields":["col",...],"Data":{"col":val,...}} — NOT flat objects.
|
||||||
|
// The read clients must decode each row via apiRow and pull named fields out
|
||||||
|
// of Data, or every field comes back empty (count still correct).
|
||||||
|
func TestOrderMap_RowsUseColumnarShape(t *testing.T) {
|
||||||
|
// Capture what schedulerclient actually requests, then answer with a
|
||||||
|
// real apiserver-shaped hosts response (captured live from t1).
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
io.WriteString(w, `{
|
||||||
|
"code": 200, "msg": "Success",
|
||||||
|
"data": {
|
||||||
|
"count": 1,
|
||||||
|
"rows": [{
|
||||||
|
"Fields": ["host_name","host_status","host_model","host_type","host_group","flag","cpu_factor","num_cpus","num_cores","num_threads","dispt_window","max_slot","max_mem","max_swap","max_tmp","num_disk","num_run","num_ssusp","num_ususp","num_done","topology","sched_stop"],
|
||||||
|
"Data": {
|
||||||
|
"host_name":"t1","host_status":"OK","host_model":"x86_64-linux-gnu",
|
||||||
|
"host_type":"x86_64-linux-gnu","host_group":"","flag":0,"cpu_factor":1,
|
||||||
|
"num_cpus":8,"num_cores":0,"num_threads":0,"dispt_window":null,
|
||||||
|
"max_slot":0,"max_mem":8224821248,"max_swap":1024,"max_tmp":2048,
|
||||||
|
"num_disk":1,"num_run":0,"num_ssusp":0,"num_ususp":0,"num_done":0,
|
||||||
|
"topology":null,"sched_stop":null
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
hc := NewClient(srv.URL, "test-token")
|
||||||
|
nodes, err := hc.ListNodes(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListNodes error: %v", err)
|
||||||
|
}
|
||||||
|
if len(nodes) != 1 {
|
||||||
|
t.Fatalf("len(nodes) = %d, want 1", len(nodes))
|
||||||
|
}
|
||||||
|
n := nodes[0]
|
||||||
|
if n.Name != "t1" {
|
||||||
|
t.Errorf("node.Name = %q, want t1", n.Name)
|
||||||
|
}
|
||||||
|
if n.Status != "idle" {
|
||||||
|
t.Errorf("node.Status = %q, want idle (OK → idle)", n.Status)
|
||||||
|
}
|
||||||
|
if n.CoresTotal != 8 {
|
||||||
|
t.Errorf("node.CoresTotal = %d, want 8", n.CoresTotal)
|
||||||
|
}
|
||||||
|
if n.MemoryBytesTotal != 8224821248*mbToBytes {
|
||||||
|
t.Errorf("node.MemoryBytesTotal = %d, want %d", n.MemoryBytesTotal, 8224821248*mbToBytes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListQueues_OrderMapShape(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
io.WriteString(w, `{
|
||||||
|
"code": 200, "msg": "Success",
|
||||||
|
"data": {
|
||||||
|
"count": 4,
|
||||||
|
"rows": [{
|
||||||
|
"Fields": ["queue_name","queue_status","priority","description","num_pend","num_run","num_ssusp","num_ususp","num_done","fs_info"],
|
||||||
|
"Data": {"queue_name":"night","queue_status":"open:active","priority":30,"description":"","num_pend":0,"num_run":0,"num_ssusp":0,"num_ususp":0,"num_done":0,"fs_info":null}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
hc := NewClient(srv.URL, "test-token")
|
||||||
|
queues, err := hc.ListQueues(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListQueues error: %v", err)
|
||||||
|
}
|
||||||
|
if len(queues) != 1 {
|
||||||
|
t.Fatalf("len(queues) = %d, want 1", len(queues))
|
||||||
|
}
|
||||||
|
q := queues[0]
|
||||||
|
if q.Name != "night" {
|
||||||
|
t.Errorf("queue.Name = %q, want night", q.Name)
|
||||||
|
}
|
||||||
|
if q.Status != "active" {
|
||||||
|
t.Errorf("queue.Status = %q, want active (open:active → active)", q.Status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListJobs_OrderMapShape(t *testing.T) {
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
io.WriteString(w, `{
|
||||||
|
"code": 200, "msg": "Success",
|
||||||
|
"data": {
|
||||||
|
"count": 3,
|
||||||
|
"rows": [{
|
||||||
|
"Fields": ["job_id","array_idx","unique_job_id","submit_user","job_status","queue_name","from_host","exec_hosts","job_name","command","submit_time"],
|
||||||
|
"Data": {"job_id":73929906127372288,"array_idx":0,"unique_job_id":73929906127372288,"submit_user":"scheduler-agent","job_status":"PEND","queue_name":"normal","from_host":"","exec_hosts":null,"job_name":"verify1","command":"sleep 30","submit_time":1785575952}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
hc := NewClient(srv.URL, "test-token")
|
||||||
|
jobs, page, err := hc.ListJobs(context.Background(), JobFilter{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ListJobs error: %v", err)
|
||||||
|
}
|
||||||
|
if page.Total != 3 {
|
||||||
|
t.Errorf("page.Total = %d, want 3", page.Total)
|
||||||
|
}
|
||||||
|
if len(jobs) != 1 {
|
||||||
|
t.Fatalf("len(jobs) = %d, want 1", len(jobs))
|
||||||
|
}
|
||||||
|
j := jobs[0]
|
||||||
|
if j.ID != "73929906127372288" {
|
||||||
|
t.Errorf("job.ID = %q, want 73929906127372288", j.ID)
|
||||||
|
}
|
||||||
|
if j.Name != "verify1" {
|
||||||
|
t.Errorf("job.Name = %q, want verify1", j.Name)
|
||||||
|
}
|
||||||
|
if j.Status != JobStatusPending {
|
||||||
|
t.Errorf("job.Status = %q, want pending", j.Status)
|
||||||
|
}
|
||||||
|
if j.Queue != "normal" {
|
||||||
|
t.Errorf("job.Queue = %q, want normal", j.Queue)
|
||||||
|
}
|
||||||
|
if j.Owner != "scheduler-agent" {
|
||||||
|
t.Errorf("job.Owner = %q, want scheduler-agent", j.Owner)
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue