fix(apiserver): job_status notnull filter matches every job (bug-1504)

`xcecli jobs list -a` returned "no job found" even when jobs existed.
FilterRows routes every condition on a bitmask field (job_status /
host_status) to MatchesBitmaskCondition, which only implemented OpEQ/OpNE
bitmask semantics — the `-a` filter compiles to `job_status notnull`
(OpNOTNULL), which fell through to `return false` and filtered out every
job. (The default list's `notin(EXIT,DONE)` is compiled to OpEQ(mask), so
it was unaffected.)

Add OpISNULL/OpNOTNULL branches to MatchesBitmaskCondition that test the
raw field value. New query_test.go: 3 regression tests (notnull matches
all, null matches none, bitmask notin path unaffected), RED->GREEN.
Verified live: `xcecli jobs list -a` now lists all jobs including DONE.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
lei.chen 2026-08-05 01:43:07 +00:00
parent 60ebf4af72
commit c630809116
4 changed files with 116 additions and 1 deletions

View file

@ -78,8 +78,18 @@ func MatchesCondition(row utils.OrderMap, cond utils.Condition) bool {
// MatchesBitmaskCondition handles bitmask fields (job_status, host_status).
// For bitmask EQ: (val & mask) != 0 means the status is set.
// For bitmask NE: (val & mask) == 0 means the status is NOT set.
// null/notnull are handled on the raw field value (bug-1504: job_status
// notnull previously fell through to `return false`, so `jobs list -a`
// filtered out every job).
func MatchesBitmaskCondition(row utils.OrderMap, field string, bitmask int64, op utils.OperatorType) bool {
val := toInt64(row.Data[field])
fieldVal := row.Data[field]
switch op {
case utils.OpISNULL:
return fieldVal == nil
case utils.OpNOTNULL:
return fieldVal != nil
}
val := toInt64(fieldVal)
result := val & bitmask
switch op {
case utils.OpNE: // bitmask "eq" is stored as OpNE (val & mask != 0)

View file

@ -0,0 +1,85 @@
package store
import (
"testing"
"apiserver/utils"
)
// makeJobRow builds a row in the OrderMap shape QueryData produces for a job.
func makeJobRow(jobid int64, status int) utils.OrderMap {
return utils.OrderMap{
Fields: []string{"job_id", "job_status", "submit_user", "queue_name"},
Data: map[string]interface{}{
"job_id": jobid,
"job_status": status,
"submit_user": "admin",
"queue_name": "normal",
},
}
}
// bug-1504: `xcecli jobs list -a` (job_status notnull) filtered out EVERY job.
// job_status is a bitmask field, so FilterRows routed the OpNOTNULL condition
// to MatchesBitmaskCondition, which only implements OpEQ/OpNE bitmask
// semantics and fell through to `return false` — the "all jobs" query always
// returned "no job found".
func TestFilterRows_JobStatusNotNull_MatchesAllJobs(t *testing.T) {
rows := []utils.OrderMap{
makeJobRow(1, 0x02), // RUN
makeJobRow(2, 0x04), // EXIT
makeJobRow(3, 0x08), // DONE
}
cond := utils.Condition{
Column: "job_status",
Operator: utils.OpNOTNULL,
Value: utils.Value{Type: utils.TypeInt, Value: int64(0)},
}
got := FilterRows(rows, []utils.Condition{cond})
if len(got) != len(rows) {
t.Fatalf("job_status notnull: got %d rows, want %d (all jobs have a status)", len(got), len(rows))
}
}
func TestFilterRows_JobStatusIsNull_MatchesNothing(t *testing.T) {
rows := []utils.OrderMap{
makeJobRow(1, 0x02),
makeJobRow(2, 0x04),
}
cond := utils.Condition{
Column: "job_status",
Operator: utils.OpISNULL,
Value: utils.Value{Type: utils.TypeInt, Value: int64(0)},
}
got := FilterRows(rows, []utils.Condition{cond})
if len(got) != 0 {
t.Fatalf("job_status null: got %d rows, want 0", len(got))
}
}
// Regression guard: the default list filter (job_status notin EXIT/DONE →
// bitmask OpEQ with mask 0x0C) must keep working after the null/notnull fix.
func TestFilterRows_JobStatusNotinExitDone(t *testing.T) {
rows := []utils.OrderMap{
makeJobRow(1, 0x02), // RUN — should survive
makeJobRow(2, 0x04), // EXIT — filtered
makeJobRow(3, 0x0C), // EXIT|DONE — filtered
}
// notin (EXIT,DONE) on a bitmask field is compiled to OpEQ with mask
// EXIT|DONE = 0x0C: keep rows where (val & mask) == 0.
cond := utils.Condition{
Column: "job_status",
Operator: utils.OpEQ,
Value: utils.Value{Type: utils.TypeInt, Value: int64(0x0C)},
}
got := FilterRows(rows, []utils.Condition{cond})
if len(got) != 1 || got[0].Data["job_status"] != 0x02 {
t.Fatalf("job_status notin(EXIT,DONE): got %d rows, want exactly the RUN job", len(got))
}
}