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 MachineNotFoundtest (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.ManualHealthCheckAsyncsignature:Task→Task<ManualHealthCheckResult>. Existingawait svc.ManualHealthCheckAsync(...)callers (5+ in prod + tests) continue to compile and run unchanged (T is just discarded at await). -
RunMachineHealthCheckResponse: now derives fromSquidResponse<T>instead of bareSquidResponse. The base class properties (code, msg) are preserved; the newDatafield 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