From eacc229e91c190b89d0fbee7e53f39757aebb868 Mon Sep 17 00:00:00 2001 From: Hongming Wang Date: Thu, 30 Apr 2026 10:15:54 -0700 Subject: [PATCH] =?UTF-8?q?test(e2e):=20fix=20/registry/register=20payload?= =?UTF-8?q?=20=E2=80=94=20id=20(not=20workspace=5Fid)=20+=20agent=5Fcard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The new external-runtime regression test had two payload bugs that made step 5 fail with HTTP 400 on its first run: 1. Field name: sent {"workspace_id":...} but RegisterPayload (workspace- server/internal/models/workspace.go:58) declares `id` with binding:"required" — workspace_id is the heartbeat payload's field, not register's. 2. Missing required field: agent_card has binding:"required" and was absent. ShouldBindJSON 400'd before any handler logic ran, which is why the body said nothing useful. Why this got past local verification: the test was written from memory of the heartbeat shape, never run end-to-end before pushing, and curl with --fail-with-body prints the body to stdout but exit-22's under set -e — the body was suppressed before the log line could fire. Fix: - Send `id` + a minimal valid agent_card ({name, skills:[{id,name}]}) matching the canonical shape from tests/e2e/test_api.sh:96. - Pull the body into REGISTER_BODY shared between steps 5 and 7 so drift between the two register calls is impossible. - Drop --fail-with-body for these two calls and append HTTP_CODE via curl -w so the body is always visible when the call non-200s. The explicit grep for HTTP_CODE=200 + ||true on curl preserves the fail-fast contract. - Inline payload contract comment pointing at RegisterPayload so the next person editing this doesn't repeat the heartbeat-confusion mistake. The url=https://example.invalid:443 is fine: runtime=external resolves to poll mode (registry.go:resolveDeliveryMode case 3), and validateAgentURL only fires for push. Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/e2e/test_staging_external_runtime.sh | 28 +++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/tests/e2e/test_staging_external_runtime.sh b/tests/e2e/test_staging_external_runtime.sh index 0099393d..4876b4c5 100755 --- a/tests/e2e/test_staging_external_runtime.sh +++ b/tests/e2e/test_staging_external_runtime.sh @@ -257,12 +257,25 @@ ok "DB row stored as awaiting_agent (proof migration 046 applied)" # PR #2382. log "5/8 Registering workspace via /registry/register..." [ -z "$WS_AUTH_TOKEN" ] && fail "No workspace auth token returned — register impossible" -REGISTER_RESP=$(curl "${CURL_COMMON[@]}" -X POST "$TENANT_URL/registry/register" \ +# Payload contract (workspace-server/internal/models/workspace.go RegisterPayload): +# id — required, the workspace UUID (NOT "workspace_id" — that's the +# heartbeat payload field; mixing them yields a 400 from +# ShouldBindJSON because `id` has binding:"required"). +# agent_card — required (binding:"required"); minimal valid card is name+skills. +# url — only validated for push-mode workspaces; runtime=external +# resolves to poll (registry.go:resolveDeliveryMode), so +# example.invalid is accepted as a placeholder URL the +# platform never dispatches to. +REGISTER_BODY=$(printf '{"id":"%s","url":"https://example.invalid:443","agent_card":{"name":"e2e-ext","skills":[{"id":"echo","name":"Echo"}]}}' "$WS_ID") +# Disable --fail-with-body for this one call so a 4xx surfaces the response +# body (the bare CURL_COMMON would `set -e`-kill before we could log it). +REGISTER_RESP=$(curl -sS --max-time 30 -w "\nHTTP_CODE=%{http_code}" -X POST "$TENANT_URL/registry/register" \ -H "Authorization: Bearer $WS_AUTH_TOKEN" \ -H "X-Molecule-Org-Id: $ORG_ID" \ -H "Content-Type: application/json" \ - -d "{\"workspace_id\":\"$WS_ID\",\"url\":\"https://example.invalid:443\"}") -log " register response: $(echo "$REGISTER_RESP" | head -c 200)" + -d "$REGISTER_BODY") || true +log " register response: $(echo "$REGISTER_RESP" | head -c 300)" +echo "$REGISTER_RESP" | grep -q "HTTP_CODE=200" || fail "register returned non-200 — see body above" GET_RESP=$(tenant_call GET "/workspaces/$WS_ID") ONLINE_STATUS=$(echo "$GET_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status',''))") @@ -296,12 +309,15 @@ ok "Heartbeat-staleness sweep transitioned online → awaiting_agent (proof heal # This proves the awaiting_agent state is recoverable (re-registrable), # which is the whole point of using it instead of 'offline'. log "7/8 Re-registering after stale → confirming recovery to online..." -REREG_RESP=$(curl "${CURL_COMMON[@]}" -X POST "$TENANT_URL/registry/register" \ +# Same payload contract as step 5 (id + agent_card both required). See note +# there for why workspace_id would 400. +REREG_RESP=$(curl -sS --max-time 30 -w "\nHTTP_CODE=%{http_code}" -X POST "$TENANT_URL/registry/register" \ -H "Authorization: Bearer $WS_AUTH_TOKEN" \ -H "X-Molecule-Org-Id: $ORG_ID" \ -H "Content-Type: application/json" \ - -d "{\"workspace_id\":\"$WS_ID\",\"url\":\"https://example.invalid:443\"}") -log " re-register response: $(echo "$REREG_RESP" | head -c 200)" + -d "$REGISTER_BODY") || true +log " re-register response: $(echo "$REREG_RESP" | head -c 300)" +echo "$REREG_RESP" | grep -q "HTTP_CODE=200" || fail "re-register returned non-200 — see body above" GET_RESP=$(tenant_call GET "/workspaces/$WS_ID") RECOVERED_STATUS=$(echo "$GET_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('status',''))")