feat(debug): drop dead confirm step from --nous upload (stateless NAS)

NAS PR #349 (merged) ships a stateless presigned-PUT endpoint: the only
route is POST /api/diagnostics/upload-url, and the object's existence in S3
is the only state. There is no /api/diagnostics/confirm route — confirming
live against the merged preview returns 404.

The client's confirm_upload() therefore fired a guaranteed-404 request on
every --nous upload (harmless, since errors were swallowed, but dead).
Remove it and simplify share_to_nous() to the 2-step mint + PUT flow that
matches the shipped contract. Drop the corresponding TestConfirmUpload class
and confirm assertions; add a test that the share succeeds even when the
response carries no id (we no longer depend on it).

The separately-flagged cross-repo requirement from #349's review --
sizeBytes is now REQUIRED and signed into the presigned URL's ContentLength
-- was already satisfied: share_to_nous() sends len(bundle) as sizeBytes and
urllib sets a matching Content-Length on the PUT. Verified against the live
merged preview (missing sizeBytes -> 400 invalid_body; present -> 503 dark).

Tested: pytest tests/hermes_cli/test_diagnostics_upload.py tests/hermes_cli/test_debug.py -> 95 passed.
This commit is contained in:
Ben 2026-06-08 15:13:03 +10:00 committed by Teknium
parent 51eeb70cb8
commit 89653db403
2 changed files with 22 additions and 81 deletions

View file

@ -10,8 +10,12 @@ Google-OAuth-gated viewer.
Flow:
1. POST {NAS_BASE}/api/diagnostics/upload-url {uploadUrl, viewUrl, id, ...}
(the request body carries ``sizeBytes``; NAS signs it into the presigned
URL's ``ContentLength``, so the PUT must send exactly that many bytes)
2. PUT <uploadUrl> (the gzipped bundle, Content-Type application/gzip)
3. POST {NAS_BASE}/api/diagnostics/confirm (best-effort; failures swallowed)
NAS is stateless the object's existence in S3 is the only state, so there is
no confirm/callback step.
Uses stdlib ``urllib`` only, matching ``debug.py`` style no third-party deps.
"""
@ -115,40 +119,15 @@ def put_bundle(
raise RuntimeError(f"diagnostics bundle PUT failed: HTTP {status}")
def confirm_upload(upload_id: str, size_bytes: int) -> None:
"""Best-effort notify NAS that the upload completed.
Purely advisory the object's existence in S3 is the source of truth
(NAS is stateless per the design), so any failure here is swallowed and
must NOT abort a share that already succeeded.
"""
try:
payload = json.dumps(
{"id": upload_id, "sizeBytes": int(size_bytes)}
).encode("utf-8")
req = urllib.request.Request(
f"{NAS_BASE}/api/diagnostics/confirm",
data=payload,
method="POST",
headers={
"Content-Type": "application/json",
"User-Agent": _USER_AGENT,
},
)
with urllib.request.urlopen(req, timeout=_REQUEST_TIMEOUT):
pass
except Exception:
# Confirm is advisory only — never let it surface to the caller.
pass
def share_to_nous(report_bundle: bytes) -> dict:
"""Orchestrate the full Nous-S3 upload of a gzipped *report_bundle*.
Returns the dict from :func:`request_upload_url` (which carries
``viewUrl`` / ``id`` / expiry metadata) so the caller can print the
viewer link. Raises on any failure of the required steps (mint URL or
PUT); the confirm step is best-effort and never raises.
Two steps: mint a presigned PUT URL (sending the exact ``sizeBytes`` NAS
signs into the URL's ``ContentLength``), then PUT the bundle. NAS is
stateless the object's existence in S3 is the only state, so there is no
confirm/callback step. Returns the dict from :func:`request_upload_url`
(which carries ``viewUrl`` / ``id`` / expiry metadata) so the caller can
print the viewer link. Raises on any failure of either step.
"""
size_bytes = len(report_bundle)
info = request_upload_url(
@ -156,8 +135,4 @@ def share_to_nous(report_bundle: bytes) -> dict:
)
put_bundle(info["uploadUrl"], report_bundle, content_type="application/gzip")
upload_id = info.get("id")
if upload_id:
confirm_upload(upload_id, size_bytes)
return info

View file

@ -176,43 +176,12 @@ class TestPutBundle:
put_bundle("https://u", b"data")
# ---------------------------------------------------------------------------
# confirm_upload
# ---------------------------------------------------------------------------
class TestConfirmUpload:
def test_confirm_posts(self):
from hermes_cli.diagnostics_upload import confirm_upload
resp = _resp(status=200, body=b"")
with patch(
"hermes_cli.diagnostics_upload.urllib.request.urlopen",
return_value=resp,
) as urlopen:
confirm_upload("abc-123", 1024)
req = urlopen.call_args[0][0]
assert req.method == "POST"
assert req.full_url.endswith("/api/diagnostics/confirm")
sent = json.loads(req.data.decode())
assert sent == {"id": "abc-123", "sizeBytes": 1024}
def test_confirm_failure_is_swallowed(self):
from hermes_cli.diagnostics_upload import confirm_upload
with patch(
"hermes_cli.diagnostics_upload.urllib.request.urlopen",
side_effect=urllib.error.URLError("connection refused"),
):
# Must NOT raise.
confirm_upload("abc-123", 1024)
# ---------------------------------------------------------------------------
# share_to_nous (orchestration)
# ---------------------------------------------------------------------------
class TestShareToNous:
def test_orchestrates_request_put_confirm(self):
def test_orchestrates_request_then_put(self):
from hermes_cli import diagnostics_upload as mod
info = {
@ -224,38 +193,35 @@ class TestShareToNous:
blob = b"gzipped-bundle"
with patch.object(mod, "request_upload_url", return_value=info) as req, \
patch.object(mod, "put_bundle") as put, \
patch.object(mod, "confirm_upload") as confirm:
patch.object(mod, "put_bundle") as put:
result = mod.share_to_nous(blob)
assert result == info
req.assert_called_once()
# request was told the real byte size
# request was told the real byte size (NAS signs it into ContentLength)
assert req.call_args.kwargs["size_bytes"] == len(blob)
# PUT got the signed URL + the exact blob
put.assert_called_once_with(
info["uploadUrl"], blob, content_type="application/gzip"
)
confirm.assert_called_once_with("id-9", len(blob))
def test_put_failure_propagates(self):
from hermes_cli import diagnostics_upload as mod
info = {"id": "id-9", "uploadUrl": "https://u", "viewUrl": "v"}
with patch.object(mod, "request_upload_url", return_value=info), \
patch.object(mod, "put_bundle", side_effect=RuntimeError("PUT failed")), \
patch.object(mod, "confirm_upload") as confirm:
patch.object(mod, "put_bundle", side_effect=RuntimeError("PUT failed")):
with pytest.raises(RuntimeError):
mod.share_to_nous(b"data")
# confirm never runs if PUT failed
confirm.assert_not_called()
def test_confirm_skipped_without_id(self):
def test_share_succeeds_without_id_in_response(self):
from hermes_cli import diagnostics_upload as mod
# NAS is stateless and there is no confirm step, so the share must
# succeed regardless of whether the response carries an ``id``.
info = {"uploadUrl": "https://u", "viewUrl": "v"} # no id
with patch.object(mod, "request_upload_url", return_value=info), \
patch.object(mod, "put_bundle"), \
patch.object(mod, "confirm_upload") as confirm:
mod.share_to_nous(b"data")
confirm.assert_not_called()
patch.object(mod, "put_bundle") as put:
result = mod.share_to_nous(b"data")
assert result == info
put.assert_called_once()