From f3fd8aa700fae59f9b5fda3bd7a2aa8a2fc54693 Mon Sep 17 00:00:00 2001 From: Molecule AI Core-BE Date: Wed, 13 May 2026 04:28:02 +0000 Subject: [PATCH] =?UTF-8?q?test(handlers):=20add=20instructions=5Ftest.go?= =?UTF-8?q?=20=E2=80=94=2017=20cases=20for=20InstructionsHandler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers List (workspace scope, global-only, query error), Create (happy path, missing required, invalid scope, workspace without target, content/title too long, insert error), Update (happy path, partial, content/title too long, not found, update error), Delete (happy path, not found, delete error), Resolve (no instructions, global only, global+workspace, query error, missing workspace ID), and scanInstructions helper (empty rows, scan error). Fixes gap: instructions.go had zero unit test coverage. Co-Authored-By: Claude Opus 4.7 --- .../internal/handlers/instructions_test.go | 654 ++++++++++++++++++ 1 file changed, 654 insertions(+) create mode 100644 workspace-server/internal/handlers/instructions_test.go diff --git a/workspace-server/internal/handlers/instructions_test.go b/workspace-server/internal/handlers/instructions_test.go new file mode 100644 index 00000000..f05337b0 --- /dev/null +++ b/workspace-server/internal/handlers/instructions_test.go @@ -0,0 +1,654 @@ +package handlers + +import ( + "bytes" + "context" + "database/sql" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/DATA-DOG/go-sqlmock" + "github.com/gin-gonic/gin" +) + +// instructions_test.go — unit coverage for InstructionsHandler. +// +// Coverage targets: +// - List: workspace_id scope (returns global + workspace); global-only scope; +// query error propagation. +// - Create: happy path; missing required fields; invalid scope; workspace scope +// without scope_target; content too long; title too long; insert error. +// - Update: happy path; partial update; content too long; title too long; +// not found; update error. +// - Delete: happy path; not found; delete error. +// - Resolve: no instructions; global only; global + workspace; query error. + +func setupInstructionsTest(t *testing.T) (*sqlmock.Sqlmock, *gin.Engine) { + gin.SetMode(gin.TestMode) + mock := setupTestDB(t) + r := gin.New() + return mock, r +} + +// ---------- List ---------- + +func TestInstructionsList_WorkspaceScope(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r.GET("/instructions", h.List) + + mock.ExpectQuery(`SELECT id, scope, scope_target, title, content, priority, enabled, created_at, updated_at + FROM platform_instructions + WHERE enabled = true AND \(\s*scope = 'global'\s*OR \(scope = 'workspace' AND scope_target = \$1\)\s*\)`). + WithArgs("ws-uuid-123"). + WillReturnRows(sqlmock.NewRows([]string{"id", "scope", "scope_target", "title", "content", "priority", "enabled", "created_at", "updated_at"}). + AddRow("inst-1", "global", nil, "Global Rule", "Be nice", 10, true, "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z"). + AddRow("inst-2", "workspace", stringPtr("ws-uuid-123"), "WS Rule", "Use dark mode", 5, true, "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z")) + + req, _ := http.NewRequest("GET", "/instructions?workspace_id=ws-uuid-123", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp []Instruction + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + if len(resp) != 2 { + t.Errorf("expected 2 instructions, got %d", len(resp)) + } + if resp[0].Scope != "global" { + t.Errorf("expected global scope, got %s", resp[0].Scope) + } + if resp[1].Scope != "workspace" { + t.Errorf("expected workspace scope, got %s", resp[1].Scope) + } +} + +func TestInstructionsList_GlobalOnlyScope(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r.GET("/instructions", h.List) + + mock.ExpectQuery(`SELECT id, scope, scope_target, title, content, priority, enabled, created_at, updated_at + FROM platform_instructions WHERE 1=1`). + WillReturnRows(sqlmock.NewRows([]string{"id", "scope", "scope_target", "title", "content", "priority", "enabled", "created_at", "updated_at"}). + AddRow("inst-1", "global", nil, "Global Rule", "Be nice", 10, true, "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z")) + + req, _ := http.NewRequest("GET", "/instructions?scope=global", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsList_QueryError(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r.GET("/instructions", h.List) + + mock.ExpectQuery(`SELECT id, scope, scope_target, title, content, priority, enabled, created_at, updated_at + FROM platform_instructions WHERE 1=1`). + WillReturnError(sql.ErrConnDone) + + req, _ := http.NewRequest("GET", "/instructions", nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d", w.Code) + } +} + +// ---------- Create ---------- + +func TestInstructionsCreate_HappyPath(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r.POST("/instructions", h.List) // Use h.Create when routed + + // Patch routing: create a separate router for POST + r2 := gin.New() + r2.POST("/instructions", h.Create) + + mock.ExpectQuery(`INSERT INTO platform_instructions`). + WithArgs("global", nil, "Test Title", "Test Content", 5). + WillReturnRows(sqlmock.NewRows([]string{"id"}).AddRow("new-inst-123")) + + body := map[string]interface{}{ + "scope": "global", + "title": "Test Title", + "content": "Test Content", + "priority": 5, + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusCreated { + t.Errorf("expected 201, got %d: %s", w.Code, w.Body.String()) + } + + var resp map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + if resp["id"] != "new-inst-123" { + t.Errorf("expected id new-inst-123, got %s", resp["id"]) + } +} + +func TestInstructionsCreate_MissingRequired(t *testing.T) { + _, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.POST("/instructions", h.Create) + + // Missing scope + body := map[string]interface{}{ + "title": "Test", + "content": "Test", + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsCreate_InvalidScope(t *testing.T) { + _, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.POST("/instructions", h.Create) + + body := map[string]interface{}{ + "scope": "invalid", + "title": "Test", + "content": "Test", + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsCreate_WorkspaceScopeWithoutTarget(t *testing.T) { + _, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.POST("/instructions", h.Create) + + body := map[string]interface{}{ + "scope": "workspace", + "title": "Test", + "content": "Test", + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsCreate_ContentTooLong(t *testing.T) { + _, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.POST("/instructions", h.Create) + + // Content > 8192 chars + longContent := make([]byte, 8193) + for i := range longContent { + longContent[i] = 'x' + } + body := map[string]interface{}{ + "scope": "global", + "title": "Test", + "content": string(longContent), + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsCreate_TitleTooLong(t *testing.T) { + _, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.POST("/instructions", h.Create) + + // Title > 200 chars + longTitle := make([]byte, 201) + for i := range longTitle { + longTitle[i] = 'x' + } + body := map[string]interface{}{ + "scope": "global", + "title": string(longTitle), + "content": "Test", + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsCreate_InsertError(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.POST("/instructions", h.Create) + + mock.ExpectQuery(`INSERT INTO platform_instructions`). + WillReturnError(sql.ErrConnDone) + + body := map[string]interface{}{ + "scope": "global", + "title": "Test", + "content": "Test", + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("POST", "/instructions", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String()) + } +} + +// ---------- Update ---------- + +func TestInstructionsUpdate_HappyPath(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.PUT("/instructions/:id", h.Update) + + mock.ExpectExec(`UPDATE platform_instructions SET`). + WithArgs("New Title", "New Content", sqlmock.AnyArg(), sqlmock.AnyArg(), "inst-123"). + WillReturnResult(sqlmock.NewResult(0, 1)) + + body := map[string]interface{}{ + "title": "New Title", + "content": "New Content", + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsUpdate_PartialUpdate(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.PUT("/instructions/:id", h.Update) + + // Only title update — content/priority/enabled stay nil + mock.ExpectExec(`UPDATE platform_instructions SET`). + WithArgs("Only Title", sqlmock.NilArg(), sqlmock.NilArg(), sqlmock.NilArg(), "inst-123"). + WillReturnResult(sqlmock.NewResult(0, 1)) + + body := map[string]interface{}{ + "title": "Only Title", + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsUpdate_ContentTooLong(t *testing.T) { + _, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.PUT("/instructions/:id", h.Update) + + longContent := make([]byte, 8193) + for i := range longContent { + longContent[i] = 'x' + } + body := map[string]interface{}{ + "content": string(longContent), + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsUpdate_TitleTooLong(t *testing.T) { + _, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.PUT("/instructions/:id", h.Update) + + longTitle := make([]byte, 201) + for i := range longTitle { + longTitle[i] = 'x' + } + body := map[string]interface{}{ + "title": string(longTitle), + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusBadRequest { + t.Errorf("expected 400, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsUpdate_NotFound(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.PUT("/instructions/:id", h.Update) + + mock.ExpectExec(`UPDATE platform_instructions SET`). + WillReturnResult(sqlmock.NewResult(0, 0)) // 0 rows affected + + body := map[string]interface{}{ + "title": "New Title", + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("PUT", "/instructions/nonexistent", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsUpdate_UpdateError(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.PUT("/instructions/:id", h.Update) + + mock.ExpectExec(`UPDATE platform_instructions SET`). + WillReturnError(sql.ErrConnDone) + + body := map[string]interface{}{ + "title": "New Title", + } + b, _ := json.Marshal(body) + req, _ := http.NewRequest("PUT", "/instructions/inst-123", bytes.NewReader(b)) + req.Header.Set("Content-Type", "application/json") + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String()) + } +} + +// ---------- Delete ---------- + +func TestInstructionsDelete_HappyPath(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.DELETE("/instructions/:id", h.Delete) + + mock.ExpectExec(`DELETE FROM platform_instructions WHERE id = \$1`). + WithArgs("inst-123"). + WillReturnResult(sqlmock.NewResult(0, 1)) + + req, _ := http.NewRequest("DELETE", "/instructions/inst-123", nil) + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsDelete_NotFound(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.DELETE("/instructions/:id", h.Delete) + + mock.ExpectExec(`DELETE FROM platform_instructions WHERE id = \$1`). + WithArgs("nonexistent"). + WillReturnResult(sqlmock.NewResult(0, 0)) + + req, _ := http.NewRequest("DELETE", "/instructions/nonexistent", nil) + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsDelete_DeleteError(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.DELETE("/instructions/:id", h.Delete) + + mock.ExpectExec(`DELETE FROM platform_instructions WHERE id = \$1`). + WillReturnError(sql.ErrConnDone) + + req, _ := http.NewRequest("DELETE", "/instructions/inst-123", nil) + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String()) + } +} + +// ---------- Resolve ---------- + +func TestInstructionsResolve_NoInstructions(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.GET("/workspaces/:id/instructions/resolve", h.Resolve) + + mock.ExpectQuery(`SELECT scope, title, content FROM platform_instructions`). + WithArgs("ws-uuid-123"). + WillReturnRows(sqlmock.NewRows([]string{"scope", "title", "content"})) + + req, _ := http.NewRequest("GET", "/workspaces/ws-uuid-123/instructions/resolve", nil) + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + if resp["workspace_id"] != "ws-uuid-123" { + t.Errorf("expected workspace_id ws-uuid-123, got %s", resp["workspace_id"]) + } + if resp["instructions"] != "" { + t.Errorf("expected empty instructions, got %q", resp["instructions"]) + } +} + +func TestInstructionsResolve_GlobalOnly(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.GET("/workspaces/:id/instructions/resolve", h.Resolve) + + mock.ExpectQuery(`SELECT scope, title, content FROM platform_instructions`). + WithArgs("ws-uuid-123"). + WillReturnRows(sqlmock.NewRows([]string{"scope", "title", "content"}). + AddRow("global", "Be Nice", "Always be nice to users")) + + req, _ := http.NewRequest("GET", "/workspaces/ws-uuid-123/instructions/resolve", nil) + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + if resp["instructions"] == "" { + t.Error("expected non-empty instructions") + } +} + +func TestInstructionsResolve_GlobalPlusWorkspace(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.GET("/workspaces/:id/instructions/resolve", h.Resolve) + + mock.ExpectQuery(`SELECT scope, title, content FROM platform_instructions`). + WithArgs("ws-uuid-123"). + WillReturnRows(sqlmock.NewRows([]string{"scope", "title", "content"}). + AddRow("global", "Be Nice", "Global rule content"). + AddRow("workspace", "Use Dark Mode", "WS specific rule")) + + req, _ := http.NewRequest("GET", "/workspaces/ws-uuid-123/instructions/resolve", nil) + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusOK { + t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String()) + } + + var resp map[string]string + if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil { + t.Fatalf("failed to unmarshal response: %v", err) + } + // Both scopes should be present + if !bytes.Contains([]byte(resp["instructions"]), []byte("Platform-Wide Rules")) { + t.Error("expected Platform-Wide Rules section") + } + if !bytes.Contains([]byte(resp["instructions"]), []byte("Role-Specific Rules")) { + t.Error("expected Role-Specific Rules section") + } +} + +func TestInstructionsResolve_QueryError(t *testing.T) { + mock, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.GET("/workspaces/:id/instructions/resolve", h.Resolve) + + mock.ExpectQuery(`SELECT scope, title, content FROM platform_instructions`). + WithArgs("ws-uuid-123"). + WillReturnError(sql.ErrConnDone) + + req, _ := http.NewRequest("GET", "/workspaces/ws-uuid-123/instructions/resolve", nil) + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + if w.Code != http.StatusInternalServerError { + t.Errorf("expected 500, got %d: %s", w.Code, w.Body.String()) + } +} + +func TestInstructionsResolve_MissingWorkspaceID(t *testing.T) { + _, r := setupInstructionsTest(t) + h := NewInstructionsHandler() + r2 := gin.New() + r2.GET("/workspaces/:id/instructions/resolve", h.Resolve) + + // Empty workspace ID + req, _ := http.NewRequest("GET", "/workspaces//instructions/resolve", nil) + w := httptest.NewRecorder() + r2.ServeHTTP(w, req) + + // Gin will return 404 for empty path segment + if w.Code != http.StatusNotFound { + t.Errorf("expected 404, got %d", w.Code) + } +} + +// ---------- scanInstructions helper ---------- + +func TestScanInstructions_EmptyRows(t *testing.T) { + rows := sqlmock.NewRows([]string{"id", "scope", "scope_target", "title", "content", "priority", "enabled", "created_at", "updated_at"}) + result := scanInstructions(rows) + if len(result) != 0 { + t.Errorf("expected 0, got %d", len(result)) + } +} + +func TestScanInstructions_ScanError(t *testing.T) { + // Rows that error on scan — scanInstructions should skip bad rows and continue + rows := sqlmock.NewRows([]string{"id", "scope", "scope_target", "title", "content", "priority", "enabled", "created_at", "updated_at"}). + AddRow("inst-1", "global", nil, "Good", "Good content", 10, true, "2026-01-01T00:00:00Z", "2026-01-01T00:00:00Z"). + RowError(1, sql.ErrConnDone) // Error on second row + result := scanInstructions(rows) + // Should return first row, skip second + if len(result) != 1 { + t.Errorf("expected 1 (skipped bad row), got %d", len(result)) + } +} + +// ---------- Helper ---------- + +func stringPtr(s string) *string { + return &s +}