Skip to content

Attach structured cgminer_code and code symbol to ApiError#9

Merged
jramos merged 4 commits into
developfrom
feature/api-error-codes
Apr 25, 2026
Merged

Attach structured cgminer_code and code symbol to ApiError#9
jramos merged 4 commits into
developfrom
feature/api-error-codes

Conversation

@jramos
Copy link
Copy Markdown
Owner

@jramos jramos commented Apr 25, 2026

Summary

CgminerApiClient::ApiError now carries two structured fields alongside the existing message so callers can dispatch on a symbol instead of parsing English error strings.

  • #cgminer_code — the integer Code from cgminer's STATUS hash (e.g., 45 for access denied), or nil if the error wasn't sourced from a wire response (the gem's own access_denied? local guard fires before any wire interaction, so cgminer_code stays nil there).
  • #code — symbolic Ruby tag derived from cgminer_code via ApiError::CGMINER_CODES, falling back to :unknown for unmapped codes. Callers can also pass code: explicitly when raising from a path with no cgminer integer.

CGMINER_CODES is intentionally conservative — only 14 → :invalid_command and 45 → :access_denied, the two error codes the test suite has actually observed against real cgminer 4.11.1 fixtures. Add a row when a new code earns dispatch-site interest.

Backward-compatible by design:

  • raise ApiError, "msg" still works (sets cgminer_code: nil, code: :unknown).
  • e.message is unchanged for existing emission sites ("#{c}: #{msg}" for wire errors, "access denied" for the local guard).
  • All existing rescue clauses in cgminer_monitor and cgminer_manager (rescue CgminerApiClient::ApiError) keep working without changes.

Three commits

  1. Refactor ApiError + dedicated spec (21 examples covering legacy form, kwargs, override semantics, to_sym coercion, raise/rescue compat). Independently bisectable — emission sites still raise with the legacy form.
  2. Wire emission sites: Miner#check_status passes cgminer_code: c from the wire response; access_denied? passes code: :access_denied explicitly. Existing tests extended with assertions on the new fields, plus a new end-to-end integration test against FakeCgminer's invalid_command fixture (Code 14) that pins dispatch against real cgminer behavior.
  3. Docs: CHANGELOG [Unreleased] ### Added entry + AGENTS.md "Error handling" paragraph naming the fields and the map.

Test plan

  • bundle exec rake green: 251 examples (was 247, +4 new), 0 failures, rubocop clean
  • Coverage holds at 99.69%
  • CGMINER_CODES is frozen
  • Legacy raise ApiError, "msg" still works and produces code: :unknown
  • Wire-side STATUS=E with Code 45 produces cgminer_code: 45, code: :access_denied (asserted via Miner#check_status unit + FakeCgminer integration)
  • Wire-side STATUS=F with an unmapped code (23) produces cgminer_code: 23, code: :unknown — fallback path covered
  • Local access_denied? guard produces cgminer_code: nil, code: :access_denied — symbol matches wire-side dispatch despite no wire interaction
  • All existing rescue CgminerApiClient::Error and rescue CgminerApiClient::ApiError consumers in monitor/manager keep working (rescue class hierarchy unchanged)

Follow-ups (not in this PR)

  • Downstream dispatch in cgminer_manager's CgminerCommander and PoolManager could now case e.code to surface "access denied" vs "invalid command" differently in the audit log — out of scope for this PR; the foundation is now in place.

jramos added 4 commits April 25, 2026 07:09
ApiError now carries two structured fields alongside the existing
message: #cgminer_code (the integer Code from cgminer's STATUS
hash, or nil) and #code (a symbolic Ruby tag derived from
cgminer_code via CGMINER_CODES, falling back to :unknown).
Callers can dispatch on the symbol instead of parsing the
English message; existing rescues that read .message are
unaffected because raise(ApiError, "msg") still works.

The CGMINER_CODES map is intentionally conservative — only 14
(:invalid_command) and 45 (:access_denied), the two error codes
the test suite has actually observed against real cgminer 4.11.1
fixtures. Add a row when a new code earns dispatch-site interest.

Wire emission sites are not yet updated; that ships in the next
commit so the constructor refactor is independently bisectable.
Two emission sites now populate the structured fields added in
the previous commit:

  * Miner#check_status (the wire path, when cgminer responds with
    STATUS=E or STATUS=F) passes cgminer_code: c so the integer
    flows through verbatim. CGMINER_CODES then derives the
    symbol — :invalid_command for 14, :access_denied for 45,
    :unknown for everything else.

  * Miner::Commands::Privileged#access_denied? (the local guard
    that fires before any wire interaction when the miner isn't
    privileged) passes code: :access_denied explicitly, since
    there's no cgminer integer to attach. Callers see the same
    symbol regardless of whether the access-denied verdict came
    from the local guard or from a real wire-side Code 45.

Tests updated to cover both paths plus an end-to-end integration
test against FakeCgminer's invalid_command fixture (Code 14),
which pins the dispatch against real cgminer 4.11.1 behavior.
The :unknown fallback is also tested via the existing fatal-status
synthetic fixture (Code 23, intentionally unmapped).
CHANGELOG entry for [Unreleased] under Added explains the two
new reader methods, the conservative CGMINER_CODES map, and the
backward-compatibility posture (raise ApiError, "msg" still works,
e.message unchanged).

AGENTS.md gains a paragraph under "Error handling" naming the
fields, the map (14, 45), and the dispatch posture: callers
case e.code, never parse e.message. The errors.rb tree-line
also picks up "+ ApiError#cgminer_code, #code".
Seven items from a five-agent /pr-review-toolkit:review-pr pass:

  A. Replace rescue-in-it pattern (4 sites) with block-form
     raise_error matcher. The old form silently passes if the call
     stops raising — exactly the regression these tests exist to
     defend against. Sites: miner_spec.rb (Code 45 + Code 23),
     commands_spec.rb (access_denied?), miner_integration_spec.rb
     (FakeCgminer Code 14).

  B. Fail loud at the library boundary on non-Integer cgminer_code.
     ApiError.new("msg", cgminer_code: "45") now raises ArgumentError
     instead of silently producing code: :unknown. Miner#check_status
     stays best-effort via Integer(c, exception: false) so a future
     firmware emitting Code as a JSON string falls through to nil
     (and :unknown) without raising mid-poll. Library boundary
     strict; wire boundary lenient.

  C. Fix factual error in access_denied? comment + spec mirror.
     The old text claimed "no wire interaction"; in fact privileged
     hits the wire and discards the integer in its rescue. New
     comment names the real mechanism so future readers don't
     hunt for a wire-skipping path that doesn't exist.

  D. Tighten CGMINER_CODES freeze test to assert FrozenError on
     element mutation. be_frozen alone proves the flag is set; this
     pins the immutability contract callers actually rely on.

  E. Add precedence test for code: nil + cgminer_code: 45 →
     :access_denied. Pins the false-trail of the || chain that the
     existing "both passed" test (with truthy code:) didn't cover.

  F. Fix misleading to_sym-location comment in api_error_spec.rb.
     Old text said coercion is at the dispatch site; production does
     it in the constructor.

Plus: end-to-end FakeCgminer integration test for the wire-side
access-denied path (Code 45 + :access_denied), e.message assertion
on the invalid-command integration test, and a forward-rot note
on the unmapped-23 test.

Tests: 251 → 258 examples (+7). Rubocop clean. Coverage 99.7%.
@jramos jramos merged commit bb3e711 into develop Apr 25, 2026
6 checks passed
@jramos jramos changed the title Attach structured cgminer_code and code symbol to ApiError (r2-§1.4) Attach structured cgminer_code and code symbol to ApiError Apr 25, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant