From 6a11c96f13e64fb4b3d17fc96b8c6c30e1c3229f Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Fri, 12 Jun 2026 06:34:31 +0000 Subject: [PATCH] test(registry): regression for boot Register non-200 log (#2615) Adds two regression tests covering #2563's deferred boot_register_failed log:\n\n- TestRegister_Non200_LogsStatusCode: a 500 register path captures the log line with workspace ID and status code.\n- TestRegister_200_DoesNotLogFailure: a successful 200 register does not emit the failure log.\n\nRefs molecule-core#2615. --- .../internal/handlers/registry_test.go | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/workspace-server/internal/handlers/registry_test.go b/workspace-server/internal/handlers/registry_test.go index a7927adb7..1a309949f 100644 --- a/workspace-server/internal/handlers/registry_test.go +++ b/workspace-server/internal/handlers/registry_test.go @@ -4,6 +4,7 @@ import ( "bytes" "database/sql" "encoding/json" + "log" "net/http" "net/http/httptest" "strings" @@ -94,6 +95,109 @@ func TestRegister_DBError(t *testing.T) { } } +// TestRegister_Non200_LogsStatusCode is a regression guard for #2563 / #2615: +// when boot Register returns a non-200 status, the response code must be logged +// so operators can distinguish 401/400/403/5xx register failures. +func TestRegister_Non200_LogsStatusCode(t *testing.T) { + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + handler := NewRegistryHandler(broadcaster) + + var buf bytes.Buffer + oldOutput := log.Writer() + log.SetOutput(&buf) + defer log.SetOutput(oldOutput) + + // Same DB-error setup as TestRegister_DBError; produces a 500 response. + mock.ExpectQuery(`SELECT delivery_mode, runtime FROM workspaces WHERE id`). + WithArgs("ws-fail"). + WillReturnError(sql.ErrNoRows) + mock.ExpectExec("INSERT INTO workspaces"). + WithArgs("ws-fail", "ws-fail", "http://localhost:8000", `{"name":"test"}`, "push", ""). + WillReturnError(sql.ErrConnDone) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + body := `{"id":"ws-fail","url":"http://localhost:8000","agent_card":{"name":"test"}}` + c.Request = httptest.NewRequest("POST", "/registry/register", bytes.NewBufferString(body)) + c.Request.Header.Set("Content-Type", "application/json") + + handler.Register(c) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected status 500, got %d: %s", w.Code, w.Body.String()) + } + + logs := buf.String() + if !strings.Contains(logs, "boot_register_failed status=500") { + t.Errorf("expected boot_register_failed log with status=500, got: %s", logs) + } + if !strings.Contains(logs, "workspace=ws-fail") { + t.Errorf("expected boot_register_failed log with workspace ID, got: %s", logs) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } +} + +// TestRegister_200_DoesNotLogFailure verifies that a successful boot Register +// does not emit the failure log line. +func TestRegister_200_DoesNotLogFailure(t *testing.T) { + mock := setupTestDB(t) + setupTestRedis(t) + broadcaster := newTestBroadcaster() + handler := NewRegistryHandler(broadcaster) + + var buf bytes.Buffer + oldOutput := log.Writer() + log.SetOutput(&buf) + defer log.SetOutput(oldOutput) + + // Successful first-registration path (same mock setup as the C18 bootstrap test). + mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM workspace_auth_tokens"). + WithArgs("ws-ok"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + mock.ExpectQuery(`SELECT delivery_mode, runtime FROM workspaces WHERE id`). + WithArgs("ws-ok"). + WillReturnError(sql.ErrNoRows) + mock.ExpectExec("INSERT INTO workspaces"). + WithArgs("ws-ok", "ws-ok", "http://localhost:9100", `{"name":"ok-agent"}`, "push", ""). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectQuery("SELECT url FROM workspaces WHERE id"). + WithArgs("ws-ok"). + WillReturnRows(sqlmock.NewRows([]string{"url"}).AddRow("http://localhost:9100")) + mock.ExpectExec("INSERT INTO structure_events"). + WillReturnResult(sqlmock.NewResult(0, 1)) + mock.ExpectQuery("SELECT COUNT\\(\\*\\) FROM workspace_auth_tokens"). + WithArgs("ws-ok"). + WillReturnRows(sqlmock.NewRows([]string{"count"}).AddRow(0)) + mock.ExpectExec("INSERT INTO workspace_auth_tokens"). + WithArgs("ws-ok", sqlmock.AnyArg(), sqlmock.AnyArg()). + WillReturnResult(sqlmock.NewResult(1, 1)) + + w := httptest.NewRecorder() + c, _ := gin.CreateTestContext(w) + c.Request = httptest.NewRequest("POST", "/registry/register", + bytes.NewBufferString(`{"id":"ws-ok","url":"http://localhost:9100","agent_card":{"name":"ok-agent"}}`)) + c.Request.Header.Set("Content-Type", "application/json") + + handler.Register(c) + + if w.Code != http.StatusOK { + t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) + } + + if strings.Contains(buf.String(), "boot_register_failed") { + t.Errorf("expected no boot_register_failed log on 200, got: %s", buf.String()) + } + + if err := mock.ExpectationsWereMet(); err != nil { + t.Errorf("unmet sqlmock expectations: %v", err) + } +} + // ==================== Heartbeat — offline → online recovery ==================== func TestHeartbeatHandler_OfflineToOnline(t *testing.T) { -- 2.52.0