Compare commits

...

1 Commits

Author SHA1 Message Date
fullstack-engineer bb8ccc0784 Add TestPatchAbilities coverage for workspace_abilities.go
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 9s
CI / Detect changes (pull_request) Successful in 13s
E2E API Smoke Test / detect-changes (pull_request) Successful in 20s
Harness Replays / detect-changes (pull_request) Successful in 22s
E2E Chat / detect-changes (pull_request) Successful in 24s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 23s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 27s
qa-review / approved (pull_request) Successful in 23s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 29s
security-review / approved (pull_request) Successful in 19s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 11s
CI / Python Lint & Test (pull_request) Successful in 12s
E2E Chat / E2E Chat (pull_request) Failing after 8s
Harness Replays / Harness Replays (pull_request) Successful in 10s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 8s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 1m33s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m30s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 6m14s
CI / Canvas (Next.js) (pull_request) Failing after 13m39s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CI / Platform (Go) (pull_request) Successful in 16m26s
CI / all-required (pull_request) Failing after 1s
gate-check-v3 / gate-check (pull_request) Successful in 3s
sop-tier-check / tier-check (pull_request) Successful in 3s
sop-checklist / all-items-acked (pull_request) acked: 7/7
sop-checklist / na-declarations (pull_request) N/A: (none)
Covers all branches of the PatchAbilities handler (was 0% coverage):
- Validation: invalid UUID (400), invalid JSON (400), empty body {} (400),
  neither field provided (400)
- Workspace not found: no rows (404), lookup error (404)
- Update errors: broadcast_enabled exec fails (500),
  talk_to_user_enabled exec fails (500)
- Success: broadcast_enabled=true, broadcast_enabled=false,
  talk_to_user_enabled=true, both fields together (all 200)

Refs: #1312, #1342
2026-05-16 15:17:59 +00:00
@@ -0,0 +1,272 @@
package handlers
import (
"bytes"
"context"
"database/sql"
"net/http"
"net/http/httptest"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/Molecule-AI/molecule-monorepo/platform/internal/db"
"github.com/gin-gonic/gin"
)
// setupAbilitiesDB creates a sqlmock with QueryMatcherEqual (quoted literals
// are not used by PatchAbilities but using the same pattern as
// workspace_broadcast_test.go keeps conventions consistent).
func setupAbilitiesDB(t *testing.T) sqlmock.Sqlmock {
t.Helper()
mockDB, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))
if err != nil {
t.Fatalf("failed to create sqlmock: %v", err)
}
prevDB := db.DB
db.DB = mockDB
t.Cleanup(func() { db.DB = prevDB; mockDB.Close() })
return mock
}
// buildAbilitiesCtx creates a gin.Context wired for PATCH /workspaces/:id/abilities.
func buildAbilitiesCtx(id, body string) (*gin.Context, *httptest.ResponseRecorder) {
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
req := httptest.NewRequest(http.MethodPatch, "/workspaces/"+id+"/abilities", bytes.NewBufferString(body))
req.Header.Set("Content-Type", "application/json")
c.Request = req.WithContext(context.Background())
c.Params = gin.Params{{Key: "id", Value: id}}
return c, w
}
// ─── Validation ────────────────────────────────────────────────────────────────
func TestPatchAbilities_InvalidWorkspaceID(t *testing.T) {
c, w := buildAbilitiesCtx("not-a-uuid", `{"broadcast_enabled":true}`)
PatchAbilities(c)
if w.Code != http.StatusBadRequest {
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
}
}
func TestPatchAbilities_InvalidJSON(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `not json`)
PatchAbilities(c)
if w.Code != http.StatusBadRequest {
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestPatchAbilities_EmptyBody(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{}`)
PatchAbilities(c)
if w.Code != http.StatusBadRequest {
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
}
// No DB queries should fire for an empty-body rejection.
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestPatchAbilities_BothFieldsNil(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{"other_field":true}`)
PatchAbilities(c)
if w.Code != http.StatusBadRequest {
t.Errorf("want 400, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
// ─── Workspace not found ────────────────────────────────────────────────────────
func TestPatchAbilities_WorkspaceNotFound(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{"broadcast_enabled":true}`)
// Workspace lookup returns exists=false.
mock.ExpectQuery("SELECT EXISTS(SELECT 1 FROM workspaces WHERE id = $1 AND status != 'removed')").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(false))
PatchAbilities(c)
if w.Code != http.StatusNotFound {
t.Errorf("want 404, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestPatchAbilities_WorkspaceLookupQueryError(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{"broadcast_enabled":true}`)
mock.ExpectQuery("SELECT EXISTS(SELECT 1 FROM workspaces WHERE id = $1 AND status != 'removed')").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001").
WillReturnError(sql.ErrConnDone)
PatchAbilities(c)
if w.Code != http.StatusNotFound {
t.Errorf("want 404, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
// ─── Update errors ─────────────────────────────────────────────────────────────
func TestPatchAbilities_BroadcastUpdateError(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{"broadcast_enabled":true}`)
mock.ExpectQuery("SELECT EXISTS(SELECT 1 FROM workspaces WHERE id = $1 AND status != 'removed')").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled = $2, updated_at = now() WHERE id = $1").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001", true).
WillReturnError(sql.ErrConnDone)
PatchAbilities(c)
if w.Code != http.StatusInternalServerError {
t.Errorf("want 500, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestPatchAbilities_TalkToUserUpdateError(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{"talk_to_user_enabled":false}`)
mock.ExpectQuery("SELECT EXISTS(SELECT 1 FROM workspaces WHERE id = $1 AND status != 'removed')").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec("UPDATE workspaces SET talk_to_user_enabled = $2, updated_at = now() WHERE id = $1").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001", false).
WillReturnError(sql.ErrConnDone)
PatchAbilities(c)
if w.Code != http.StatusInternalServerError {
t.Errorf("want 500, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
// ─── Success paths ─────────────────────────────────────────────────────────────
func TestPatchAbilities_BroadcastEnabledTrue(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{"broadcast_enabled":true}`)
mock.ExpectQuery("SELECT EXISTS(SELECT 1 FROM workspaces WHERE id = $1 AND status != 'removed')").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled = $2, updated_at = now() WHERE id = $1").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001", true).
WillReturnResult(sqlmock.NewResult(0, 1))
PatchAbilities(c)
if w.Code != http.StatusOK {
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestPatchAbilities_BroadcastEnabledFalse(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{"broadcast_enabled":false}`)
mock.ExpectQuery("SELECT EXISTS(SELECT 1 FROM workspaces WHERE id = $1 AND status != 'removed')").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled = $2, updated_at = now() WHERE id = $1").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001", false).
WillReturnResult(sqlmock.NewResult(0, 1))
PatchAbilities(c)
if w.Code != http.StatusOK {
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestPatchAbilities_TalkToUserEnabled(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{"talk_to_user_enabled":true}`)
mock.ExpectQuery("SELECT EXISTS(SELECT 1 FROM workspaces WHERE id = $1 AND status != 'removed')").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec("UPDATE workspaces SET talk_to_user_enabled = $2, updated_at = now() WHERE id = $1").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001", true).
WillReturnResult(sqlmock.NewResult(0, 1))
PatchAbilities(c)
if w.Code != http.StatusOK {
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}
func TestPatchAbilities_BothFields(t *testing.T) {
mock := setupAbilitiesDB(t)
c, w := buildAbilitiesCtx("bbbbbbbb-0001-0001-0001-000000000001", `{"broadcast_enabled":true,"talk_to_user_enabled":false}`)
mock.ExpectQuery("SELECT EXISTS(SELECT 1 FROM workspaces WHERE id = $1 AND status != 'removed')").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001").
WillReturnRows(sqlmock.NewRows([]string{"exists"}).AddRow(true))
mock.ExpectExec("UPDATE workspaces SET broadcast_enabled = $2, updated_at = now() WHERE id = $1").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001", true).
WillReturnResult(sqlmock.NewResult(0, 1))
mock.ExpectExec("UPDATE workspaces SET talk_to_user_enabled = $2, updated_at = now() WHERE id = $1").
WithArgs("bbbbbbbb-0001-0001-0001-000000000001", false).
WillReturnResult(sqlmock.NewResult(0, 1))
PatchAbilities(c)
if w.Code != http.StatusOK {
t.Errorf("want 200, got %d: %s", w.Code, w.Body.String())
}
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("unmet mock expectations: %v", err)
}
}