Skip to content

H3: Return structured ManualHealthCheckResult from active health-check probe

Summary

H3 of the 1.8.0 Upgrade Hardening initiative (H1 #354 + H2 #355 already merged).

Fixes the operator-reported "點了 health check 還是一樣的" UX gap. The existing /api/machines/{id}/health-check endpoint already does an active Halibut Capabilities probe + updates cache (now also DB-persisted via H2). H3 is purely about response richness — the empty success/fail shape gave operators no signal that the probe actually re-read agent metadata, so they couldn't tell what changed or what to do when it failed.

API shape change (additive, backward-compatible)

// Before (pre-H3):
{ "code": 200, "msg": "Success" }

// After (H3):
{
  "code": 200,
  "msg": "Success",
  "data": {
    "successful": true,
    "detail": "Tentacle connected — version 1.7.9, OS=Windows",
    "errorCode": null,
    "agentVersion": "1.7.9",
    "os": "Windows",
    "checkedAt": "2026-05-23T08:30:45Z"
  }
}

Pre-H3 clients reading only the success/fail signal continue to work; new clients can branch on errorCode and render fresh agentVersion / os.

Structured error codes (wire-stable, lower_snake_case)

Code When Operator action
null probe succeeded none — agent capabilities updated
machine_not_found machine ID doesn't exist check the ID; refresh fleet list
machine_disabled machine is disabled enable the machine before re-running
no_health_checker transport has no probe strategy (e.g. some SSH transports) use the manual health-check procedure for this style
agent_unreachable Halibut RPC failed (timeout, refused, etc.) check network + agent service status

Pre-H3 behaviour for machine_not_found was a thrown InvalidOperationException → generic 500 to FE. Post-H3 returns the structured result.

Test plan

  • +4 wire-stability pins on error code literals (ManualHealthCheckErrorCodesIntegrityTests)
  • +1 lower_snake_case convention drift detector
  • +4 structured-outcome scenarios (success / disabled / no_health_checker / agent_unreachable)
  • +1 retrofitted MachineNotFound test (now returns structured result instead of throwing)
  • 5554/5554 unit tests green (vs 5545/5545 baseline → +9 net new)
  • Integration/E2E: deferred to H8 (E2E matrix includes "health check populates fresh capabilities" round-trip scenarios)

Backward compatibility

  • IMachineHealthCheckService.ManualHealthCheckAsync signature: Task → Task<ManualHealthCheckResult>. Existing await svc.ManualHealthCheckAsync(...) callers (5+ in prod + tests) continue to compile and run unchanged (T is just discarded at await).
  • RunMachineHealthCheckResponse: now derives from SquidResponse<T> instead of bare SquidResponse. The base class properties (code, msg) are preserved; the new Data field is additive.
  • "Machine not found" semantic change: throw → structured result. Behavioural break, but in the direction operators want (actionable error code vs. generic 500).

What's NOT in this PR

  • H4: Upgrade lock TTL + idempotency (curing the "repeated upgrade clicks stuck me forever" failure)
  • H5: Cross-OS unified upgrade pipeline
  • H6: Agent rollback + abandonment recovery
  • H7: Role/feature capability slots (catches "IIS not installed" at plan-time)
  • H8: Comprehensive E2E test matrix

Merge request reports

Loading