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
+11 -36
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
+11 -45
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()