myxce/.wolf/bugs/bug-1680.md
Rongsong Shen d26ff8870d
feat(buglog): one Markdown record per bug, checked against a record standard
The buglog was one 27K-line JSON file whose fields covered only symptom,
root cause and fix, as free text: 2% of entries named the code version a
bug was found on, 12% the build profile, 36% a way to reproduce, and 58% of
the file was OpenWolf hook auto-detections ("Missing guard clause",
"Function not marked async" on .md files).

- .claude/skills/buglog/bug-record.md: the record standard. Eight
  sections (Summary as the title, Description, Environment, Reproduction;
  Root cause, Fix, Verification, References once fixed), each with the
  content it must contain -- e.g. Environment needs the OS, the libc the
  failing binary loads (from its ELF interpreter: glibc 2.42 in the nix
  shell vs 2.40 on the host), toolchain, build profile and commit.
- .wolf/bugs/<id>.md: 500 records migrated from buglog.json; every field
  verified to round-trip. They carry legacy: true and an explicit "not
  recorded" where the old log had no such field; component derived from
  file paths, severity from keywords (else unknown), fix_commits from
  hashes in status_note/fix that git resolves (91 of 463 fixed).
- .wolf/buglog-archive.json: the 710 hook auto-detections, verbatim.
- .wolf/buglog.json is now an index generated from the records, keeping
  the fields OpenWolf's hooks and `openwolf bug search` read.
- scripts/buglog.py rewritten: new (template + Environment collected from
  the machine and --binary), check, status (refuses a status the record
  doesn't support; fixed needs Root cause, Fix, Verification and
  --commit hashes git resolves), note, recur, set, show, list, triage,
  env, index. 29 unittest cases in scripts/tests/test_buglog.py.
- scripts/git-hooks/pre-commit runs `check --staged`: incomplete staged
  records, a legacy record hand-changed to fixed without the fix-time
  sections, a new record claiming legacy: true, or a stale index refuse
  the commit.
- scripts/gitea-sync.py reads the records: issue body = the record's
  sections plus linked fix commits (origin remote, GITEA_COMMIT_REPO to
  override), and a body hash so an issue's text is refreshed when the
  record changes.
- bug-1866 rewritten to the full standard as the worked example.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-28 03:40:11 +00:00

6.1 KiB

id status status_updated status_note component severity timestamp last_seen occurrences files tags related legacy
bug-1680 fixed 2026-09-24 apiserver data-loss 2026-08-25T03:50:00Z 2026-09-19T09:00:00Z 1
src/frontend/xcecli/filters/job_filter.go:58-60
src/frontend/apiserver/utils/types.go:262-322
src/frontend/apiserver/store/query.go:70-73
xcecli
apiserver
job_filter.go
types.go
query.go
job-status-filter
cli-bug
discovered-via-bug-1679
bug-1679
true

bug-1680: xcecli jobs list -a ("list run,done,pending,suspended jobs" -- i.e

Description

xcecli jobs list -a ("list run,done,pending,suspended jobs" -- i.e. show every job regardless of status) returns "no job found" even when jobs genuinely exist and are correctly stored with a real status. Discovered as a side effect of live-verifying bug-1679: two jobs that correctly showed PEND->RUN->DONE via the default query and via -d (done-only) both come back empty under -a specifically. Not a data-loss bug (the jobs ARE correctly stored and queryable through other flags -- -d, the default active-jobs view -- this is specifically -a's own query construction that's broken) but a real, reproducible CLI defect: -a is documented as the way to see literally everything, and it shows nothing.

Environment

Not recorded (migrated from buglog.json).

Reproduction

Not recorded (migrated from buglog.json).

Root cause

xcecli's -a flag (list.go:347, AllJobInfoFlag) makes HandleJobFilter (job_filter.go:57-60) build the filter string {job_status:notnull:} -- a notnull operator on job_status with an EMPTY value token (no argument needed for null-checks, by design) -- instead of the normal {job_status:in:(...)}/{job_status:notin:(...)} shape every other status flag (-p/-r/-d/-s) produces. Server-side, job_status/host_status get SPECIAL bitmask handling in buildIntCondition (types.go:262-322), but that special-casing only triggers for the in/notin/between operators (the if filter.Field == "job_status" || filter.Field == "host_status" branch sits inside the if filterOperator == "in" || filterOperator == "notin" || filterOperator == "between" block, types.go:263-270) -- notnull falls through to the generic scalar path (types.go:316-322), which builds a plain Condition{Column: "job_status", Operator: OpNOTNULL, Value: {TypeInt, 0}} with no bitmask awareness at all. In isolation, store/query.go's own MatchesCondition evaluator (query.go:70-73) implements OpNOTNULL correctly (fieldVal != nil), which should trivially match every job (job_status is never actually absent/nil in a stored row) -- meaning the actual break is somewhere between condition construction and evaluation for this specific field/operator combination that was not fully traced (a debugger/print-tracing session would be needed to pin the exact line): plausible candidates are (a) the query engine routing job_status conditions through a bitmask-specific code path unconditionally regardless of operator, silently misinterpreting a non-bitmask NOTNULL Condition as a bitmask EQ/NE against value 0 (which would exclude every real job_status value, all of which are nonzero bitmask flags), or (b) some other job_status-specific special-casing elsewhere in the query pipeline that only handles the in/notin/eq/ne shapes types.go's own bitmask branch produces and never anticipated a bare NOTNULL condition on this field at all.

Fix

FIXED (2026-08-25). Root cause fully pinned down (the prior entry's root_cause left the construction-to-evaluation gap untraced -- now traced exactly): src/frontend/apiserver/store/query.go's FilterRows() (the actual filter-evaluation loop, not something the prior investigation reached) routes ANY condition on job_status/host_status through the bitmask path purely based on IsBitmaskField(cond.Column) -- unconditional on the field NAME alone, never checking the condition's actual Operator. MatchesBitmaskCondition()'s own switch only implements OpEQ/OpNE (the only two shapes buildIntCondition's in/notin bitmask branch ever produces) and silently return false for any other operator -- so {job_status:notnull:} (Operator: OpNOTNULL, Value: {TypeInt, 0}, per types.go's null/notnull scalar path) gets routed into MatchesBitmaskCondition anyway, matches neither switch case, and returns false for every single row unconditionally. That's the entire bug: field-name-based routing that ignores which operator shape is actually on the condition.

Fix: FilterRows's routing condition now requires BOTH IsBitmaskField(cond.Column) AND cond.Operator being OpEQ or OpNE before taking the bitmask path; any other operator (OpNOTNULL, OpISNULL, or anything else that might ever target job_status/host_status) correctly falls through to the generic MatchesCondition evaluator, which already implements OpNOTNULL/OpISNULL correctly (fieldVal != nil / == nil) -- confirmed by the fix requiring zero changes to MatchesCondition or MatchesBitmaskCondition themselves, only the dispatch condition in FilterRows.

New src/frontend/apiserver/store/query_test.go (no test file existed for this package before -- confirmed via search, matches the prior entry's note): TestFilterRows_JobStatusNotNull_MatchesRealRows reproduces the exact bug (RED before the fix: 0 rows matched instead of 3; independently confirmed) and TestFilterRows_JobStatusBitmaskEQ_StillWorks locks in that the actual bitmask shape (OpEQ/OpNE, what every -p/-r/-d/-s status flag produces) is unaffected -- both pass after the fix. Full apiserver/store, apiserver/controllers, apiserver/models (CGO_ENABLED=0) and apiserver/backend + apiserver/backend/auth (full CGO via make test-backend) suites all pass, zero regressions.

Live-verified: rebuilt and hot-swapped the apiserver binary on a running cluster with 3 pre-existing DONE jobs (previously invisible to -a) plus one freshly-submitted job -- xcecli jobs list -a now correctly shows all 4 jobs across every status (3x DONE, 1x RUN), immediately, no restart needed.

Verification

Not recorded (migrated from buglog.json).

References

  • Related: bug-1679